From 4ed38ef06194800b4054a668ae12a42759632bda Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 09:44:13 -0400 Subject: [PATCH 1/7] Refactor --- .github/workflows/ci.yaml | 4 +- crates/solver_vrp/src/lib.rs | 78 +++++------- crates/solver_vrp/src/model.rs | 18 +-- crates/solver_vrp/src/operator.rs | 190 ++++++++++++++++++++++++++++ crates/solver_vrp/src/random.rs | 53 ++++++++ crates/solver_vrp/src/solution.rs | 5 + crates/solver_vrp/src/solver.rs | 199 +++--------------------------- 7 files changed, 312 insertions(+), 235 deletions(-) create mode 100644 crates/solver_vrp/src/operator.rs create mode 100644 crates/solver_vrp/src/random.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1bef8d..8bd03ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: ubuntu-latest + os: [ubuntu-latest] steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly @@ -51,7 +51,7 @@ jobs: run: cargo clippy --workspace --all-features -- -D warnings format: name: Format - runs-on: ubuntu-latest + runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly diff --git a/crates/solver_vrp/src/lib.rs b/crates/solver_vrp/src/lib.rs index 0e637ed..3d667a7 100644 --- a/crates/solver_vrp/src/lib.rs +++ b/crates/solver_vrp/src/lib.rs @@ -6,15 +6,17 @@ //! //! - Pickup and Delivery Problem (PDP) //! -//! ``` -//! use solver_vrp::{Constraint, Expression, Model, ModelBuilder, Objective, Operator, Plan, Solution, SolverBuilder, SolverOptions}; +//! ```rust,ignore +//! use solver_vrp::model::{Constraint, Expression, Model, ModelBuilder, Objective}; +//! use solver_vrp::operator::Operator; +//! use solver_vrp::solution::{Plan, Solution}; +//! use solver_vrp::solver::{Solver, SolverBuilder, SolverOptions}; +//! use solver_vrp::random::Random; //! //! // Implement a custom objective to add optimized features like linehaul costs to the model. -//! struct CustomObjective; -//! -//! impl Objective for CustomObjective { +//! impl Objective for ZeroObjective { //! fn name(&self) -> String { -//! String::from("Custom Objective") +//! String::from("Zero Objective") //! } //! //! // Returns the computed value of the objective for the given plan. @@ -24,40 +26,29 @@ //! } //! //! // Implement a custom constraint to enforce unique business rules in the model. -//! struct CustomConstraint; +//! struct MaxVehicleWeight((f64, f64)); //! -//! impl Constraint for CustomConstraint { +//! impl Constraint for MaxVehicleWeight { //! fn name(&self) -> String { -//! String::from("Custom Objective") +//! String::from("Max Vehicle Weight") //! } //! //! // Returns true if the plan is feasible. -//! fn is_feasible(&self, _model: &Model, _solution: &Solution, _plan: &Plan) -> bool { -//! true -//! } -//! -//! // Returns true if the constraint is temporal. -//! fn is_temporal(&self) -> bool { -//! false -//! } -//! } -//! -//! // Implement a custom expression to add new calculations to the model. -//! struct CustomExpression; -//! -//! impl Expression for CustomExpression { -//! fn name(&self) -> String { -//! String::from("Custom Expression") -//! } -//! -//! // Returns the computed value for the expression. -//! fn compute(&self, _model: &Model, _solution: &Solution, _plan: &Plan) -> f64 { -//! 0.0 +//! fn is_feasible(&self, plan: &Plan) -> bool { +//! plan.route() +//! .changes() +//! .last() +//! .map_or(true, |change| { +//! change.capacity.utilization(1) <= self.0.1 +//! }); //! } //! } //! //! // Add new operators to refine the solver's search and heuristic capabilities. -//! struct CustomOperator; +//! #![derive(Default)] +//! struct RejectEverything { +//! parameters: OperatorParameters, +//! }; //! //! impl Operator for CustomOperator { //! fn name(&self) -> String { @@ -65,17 +56,16 @@ //! } //! //! // Returns the new solution after executing the operator. -//! fn execute(&self, _model: &Model, _solution: &Solution) -> Option { -//! None +//! fn execute(&self, _model: &Model, _solution: &Solution, _random: &mut Random) -> Plan { +//! Plan::default() //! } //! } //! //! fn run() { //! // Build the model with custom components. //! let model = ModelBuilder::new() -//! .objective(CustomObjective) -//! .constraint(CustomConstraint) -//! .expression(CustomExpression) +//! .objective(CustomObjective::default()) +//! .constraint(CustomConstraint((26.0, 40_000.0))) //! .build(); //! //! // Define options for the solver. @@ -88,7 +78,7 @@ //! .options(options) //! .model(model) //! .plan(initial_solution) -//! .operator(CustomOperator) +//! .operator(CustomOperator {}) //! .build(); //! //! let best_solution = solver.solve(); @@ -116,15 +106,15 @@ //! # `Solver` //! //! The `Solver` is designed to implement specific defaults that can be extended from. Override internal -//! options and express your `Model` and `Solver` with custom objectives, constraints, expressions, -//! and operators. By default, this is a ALNS (Adaptive Large Neighborhood Search) solver that uses multiple -//! strategies to explore the solution space. +//! options and express your `Model` and `Solver` with custom objectives, constraints, and operators. By +//! default, this is a ALNS (Adaptive Large Neighborhood Search) solver that uses multiple strategies to +//! explore the solution space. //! //! # `Model` //! //! The `Model` struct represents the vehicle routing problem instance to be solved. It contains all //! of the input data as well as the definitions for the model. Inputs include stops, vehicles, and -//! a distance matrix. Objectives, constraints, and expressions are used to define and extend the model. +//! a distance matrix. //! //! # `Solution` //! @@ -158,9 +148,7 @@ //! Every model implements some number of expressions that are used for internal calculations. mod model; +mod operator; +mod random; mod solution; mod solver; - -pub use model::{Constraint, Expression, Model, ModelBuilder, Objective}; -pub use solution::{Plan, Solution}; -pub use solver::{Operator, Solver, SolverBuilder, SolverOptions}; diff --git a/crates/solver_vrp/src/model.rs b/crates/solver_vrp/src/model.rs index 3aef25d..02fc6b8 100644 --- a/crates/solver_vrp/src/model.rs +++ b/crates/solver_vrp/src/model.rs @@ -538,14 +538,14 @@ mod tests { } #[test] - fn test_dag() { - let mut dag = DirectedAcyclicGraph::with_capacity(3); - dag.add_arc(0, 1); - dag.add_arc(1, 2); - assert_eq!(dag.edges().len(), 3); - assert_eq!(dag.edges()[0], vec![1]); - assert_eq!(dag.edges()[1], vec![2]); - assert_eq!(dag.edges()[2], vec![]); - assert_eq!(dag.arcs().len(), 2); + fn test_graph() { + let mut graph = DirectedAcyclicGraph::with_capacity(3); + graph.add_arc(0, 1); + graph.add_arc(1, 2); + assert_eq!(graph.edges().len(), 3); + assert_eq!(graph.edges()[0], vec![1]); + assert_eq!(graph.edges()[1], vec![2]); + assert_eq!(graph.edges()[2], vec![]); + assert_eq!(graph.arcs().len(), 2); } } diff --git a/crates/solver_vrp/src/operator.rs b/crates/solver_vrp/src/operator.rs new file mode 100644 index 0000000..655b04a --- /dev/null +++ b/crates/solver_vrp/src/operator.rs @@ -0,0 +1,190 @@ +use crate::model::Model; +use crate::random::Random; +use crate::solution::{Plan, Solution}; + +pub trait Operator { + /// Name of the operator. + fn name(&self) -> String; + /// Executes the operator to generate a new solution. + fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan; + /// Chance of applying the operator. + fn chance(&self) -> f64 { + 1.0 + } +} + +#[derive(Default)] +pub struct Operators(Vec>); + +impl Operators { + pub fn new() -> Self { + Self::default() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn first(&self) -> Option<&dyn Operator> { + self.0.first().map(AsRef::as_ref) + } + + pub fn get(&self, index: usize) -> Option<&dyn Operator> { + self.0.get(index).map(AsRef::as_ref) + } + + pub fn push(&mut self, operator: Box) { + self.0.push(operator); + } + + pub fn iter(&self) -> std::slice::Iter<'_, Box> { + self.0.iter() + } +} + +pub enum RepairOperator { + Random(OperatorParameters), + Nearest(OperatorParameters), +} + +impl Operator for RepairOperator { + fn name(&self) -> String { + match self { + Self::Random(_) => String::from("Repair Operator (Random)"), + Self::Nearest(_) => String::from("Repair Operator (Nearest)"), + } + } + + fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { + match self { + Self::Random(p) => repair_random(model, solution, p, random), + Self::Nearest(p) => repair_nearest(model, solution, p, random), + } + } + + fn chance(&self) -> f64 { + match self { + RepairOperator::Random(p) | RepairOperator::Nearest(p) => p.chance_f64, + } + } +} + +pub enum DestroyOperator { + Random(OperatorParameters), + Nearest(OperatorParameters), +} + +impl Operator for DestroyOperator { + fn name(&self) -> String { + match self { + Self::Random(_) => String::from("Destroy Operator (Random)"), + Self::Nearest(_) => String::from("Destroy Operator (Nearest)"), + } + } + + fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { + match self { + Self::Random(p) => destroy_random(model, solution, p, random), + Self::Nearest(p) => destroy_nearest(model, solution, p, random), + } + } + + fn chance(&self) -> f64 { + match self { + DestroyOperator::Random(p) | DestroyOperator::Nearest(p) => p.chance_f64, + } + } +} + +pub enum ResetOperator { + Full(OperatorParameters), + Partial(OperatorParameters), +} + +impl Operator for ResetOperator { + fn name(&self) -> String { + match self { + Self::Full(_) => String::from("Reset Operator (Full)"), + Self::Partial(_) => String::from("Reset Operator (Partial)"), + } + } + + fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { + match self { + Self::Full(p) => reset_full(model, solution, p, random), + Self::Partial(p) => reset_partial(model, solution, p, random), + } + } + + fn chance(&self) -> f64 { + match self { + ResetOperator::Full(p) | ResetOperator::Partial(p) => p.chance_f64, + } + } +} + +fn repair_random( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +fn repair_nearest( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +fn destroy_random( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +fn destroy_nearest( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +fn reset_full( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +fn reset_partial( + _model: &Model, + _solution: &Solution, + _params: &OperatorParameters, + _random: &mut Random, +) -> Plan { + todo!() +} + +pub struct OperatorParameters { + value: f64, + chance_f64: f64, +} + +impl OperatorParameters { + #[must_use] + pub fn new(value: f64, chance_f64: f64) -> Self { + Self { value, chance_f64 } + } +} diff --git a/crates/solver_vrp/src/random.rs b/crates/solver_vrp/src/random.rs new file mode 100644 index 0000000..16ca795 --- /dev/null +++ b/crates/solver_vrp/src/random.rs @@ -0,0 +1,53 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use rand::{Rng, SeedableRng, rngs::StdRng}; + +pub struct Random { + rng: StdRng, +} + +impl Default for Random { + fn default() -> Self { + Self::new() + } +} + +impl Random { + pub fn new() -> Self { + Self::seed( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ) + } + + pub fn seed(seed: u64) -> Self { + Self { + rng: StdRng::seed_from_u64(seed), + } + } + + pub fn u32(&mut self) -> u32 { + self.rng.random() + } + + pub fn f64(&mut self) -> f64 { + self.rng.random() + } + + pub fn range_u32(&mut self, low: u32, high: u32) -> u32 { + self.rng.random_range(low..high) + } + + pub fn range_f64(&mut self, low: f64, high: f64) -> f64 { + self.rng.random_range(low..high) + } + + pub fn chance(&mut self, (numerator, denominator): (f64, f64)) -> bool { + if numerator == denominator { + return true; + } + self.f64() < (numerator / denominator) + } +} diff --git a/crates/solver_vrp/src/solution.rs b/crates/solver_vrp/src/solution.rs index f2e938d..474d57f 100644 --- a/crates/solver_vrp/src/solution.rs +++ b/crates/solver_vrp/src/solution.rs @@ -28,6 +28,11 @@ impl Solution { self.value } + #[must_use] + pub fn plan(&self, _plan: &Plan) -> Solution { + todo!() + } + #[must_use] pub fn best(self, other: Solution) -> Solution { if self.value < other.value { diff --git a/crates/solver_vrp/src/solver.rs b/crates/solver_vrp/src/solver.rs index 6c7b94d..15d93ed 100644 --- a/crates/solver_vrp/src/solver.rs +++ b/crates/solver_vrp/src/solver.rs @@ -1,15 +1,11 @@ +use core::panic; + use crate::model::Model; +use crate::operator::{Operator, Operators}; +use crate::random::Random; use crate::solution::Solution; -use rand::prelude::{Rng, SeedableRng, StdRng}; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub trait Operator { - /// Name of the operator. - fn name(&self) -> String; - /// Executes the operator to generate a new solution. - fn execute(&self, model: &Model, solution: &Solution) -> Option; -} +#[derive(Default)] pub struct Solver { model: Model, operators: Operators, @@ -42,7 +38,7 @@ impl Solver { #[must_use] pub fn solve(mut self) -> Option { - while self.options.max_iterations > self.iteration_count { + while self.iteration_count < self.options.max_iterations { self.execute_operators(); self.increment_iteration(); } @@ -56,47 +52,20 @@ impl Solver { fn execute_operators(&mut self) { let mut solution = self.solution.take().unwrap_or_default(); for op in self.operators.iter() { - if let Some(s) = op.execute(&self.model, &solution) { - solution = s.best(solution); + if !self.random.chance((op.chance(), 1.0)) { + continue; } + solution = solution + .plan(&op.execute(&self.model, &solution, &mut self.random)) + .best(solution); } self.solution = Some(solution); } } -#[derive(Default)] -pub struct Operators(Vec>); - -impl Operators { - pub fn new() -> Self { - Self::default() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn first(&self) -> Option<&dyn Operator> { - self.0.first().map(AsRef::as_ref) - } - - pub fn get(&self, index: usize) -> Option<&dyn Operator> { - self.0.get(index).map(AsRef::as_ref) - } - - pub fn push(&mut self, operator: Box) { - self.0.push(operator); - } - - pub fn iter(&self) -> std::slice::Iter<'_, Box> { - self.0.iter() - } -} - #[derive(Default)] pub struct SolverBuilder { - operators: Operators, - options: SolverOptions, + solver: Solver, } impl SolverBuilder { @@ -107,45 +76,25 @@ impl SolverBuilder { #[must_use] pub fn operator(mut self, operator: Op) -> Self { - self.operators.push(Box::new(operator)); + self.solver.operators.push(Box::new(operator)); self } #[must_use] pub fn options(mut self, options: SolverOptions) -> Self { - self.options = options; + self.solver.options = options; self } #[must_use] - pub fn model(self, model: Model) -> SolverBuilderWithModel { - SolverBuilderWithModel { - solver: Solver { - model, - operators: self.operators, - options: self.options, - solution: None, - random: Random::new(), - iteration_count: 0, - }, - } - } -} - -pub struct SolverBuilderWithModel { - solver: Solver, -} - -impl SolverBuilderWithModel { - #[must_use] - pub fn operator(mut self, operator: Op) -> Self { - self.solver.operators.push(Box::new(operator)); + pub fn model(mut self, model: Model) -> SolverBuilder { + self.solver.model = model; self } #[must_use] - pub fn options(mut self, options: SolverOptions) -> Self { - self.solver.options = options; + pub fn plan(mut self, solution: Solution) -> Self { + self.solver.solution = Some(solution); self } @@ -153,11 +102,6 @@ impl SolverBuilderWithModel { pub fn build(self) -> Solver { self.solver } - - pub fn plan(mut self, solution: Solution) -> Self { - self.solver.solution = Some(solution); - self - } } pub struct SolverOptions { @@ -179,117 +123,14 @@ impl Default for SolverOptions { } } -pub enum RepairOperator { - Random(OperatorParameters), - Nearest(OperatorParameters), -} - -impl Operator for RepairOperator { - fn name(&self) -> String { - match self { - Self::Random(_) => String::from("Random Repair Operator"), - Self::Nearest(_) => String::from("Nearest Repair Operator"), - } - } - - fn execute(&self, _model: &Model, _solution: &Solution) -> Option { - None - } -} - -pub enum DestroyOperator { - Random(OperatorParameters), - Nearest(OperatorParameters), -} - -impl Operator for DestroyOperator { - fn name(&self) -> String { - match self { - Self::Random(_) => String::from("Random Destroy Operator"), - Self::Nearest(_) => String::from("Nearest Destroy Operator"), - } - } - - fn execute(&self, _model: &Model, _solution: &Solution) -> Option { - None - } -} - -pub enum ResetOperator { - Full(OperatorParameters), - Partial(OperatorParameters), -} - -impl Operator for ResetOperator { - fn name(&self) -> String { - match self { - Self::Full(_) => String::from("Full Reset Operator"), - Self::Partial(_) => String::from("Partial Reset Operator"), - } - } - - fn execute(&self, _model: &Model, _solution: &Solution) -> Option { - None - } -} - -pub struct OperatorParameters { - value: f64, - chance: f64, -} - -impl OperatorParameters { - #[must_use] - pub fn new(value: f64, chance: f64) -> Self { - Self { value, chance } - } -} - -struct Random { - rng: StdRng, -} - -impl Random { - fn new() -> Self { - Self::seed( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(), - ) - } - - fn seed(seed: u64) -> Self { - Self { - rng: StdRng::seed_from_u64(seed), - } - } - - fn u32(&mut self) -> u32 { - self.rng.random() - } - - fn f64(&mut self) -> f64 { - self.rng.random() - } - - fn range_u32(&mut self, low: u32, high: u32) -> u32 { - self.rng.random_range(low..high) - } - - fn range_f64(&mut self, low: f64, high: f64) -> f64 { - self.rng.random_range(low..high) - } -} - #[cfg(test)] mod tests { use crate::{ - ModelBuilder, model::{ - DistanceExpression, UnplannedObjective, VehicleCapacityConstraint, + DistanceExpression, ModelBuilder, UnplannedObjective, VehicleCapacityConstraint, VehicleCompatibilityConstraint, }, + operator::{DestroyOperator, OperatorParameters, RepairOperator, ResetOperator}, }; use super::*; From 2a7b6c39b3b81904acdc2608467b5209205e4d2e Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 18:01:19 -0400 Subject: [PATCH 2/7] Clippy --- crates/solver_vrp/src/solver.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/solver_vrp/src/solver.rs b/crates/solver_vrp/src/solver.rs index 15d93ed..51645e8 100644 --- a/crates/solver_vrp/src/solver.rs +++ b/crates/solver_vrp/src/solver.rs @@ -1,5 +1,3 @@ -use core::panic; - use crate::model::Model; use crate::operator::{Operator, Operators}; use crate::random::Random; From 94587f99d39386aa06f05fc7083a288997fd44eb Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 18:30:25 -0400 Subject: [PATCH 3/7] Chore --- crates/solver_vrp/src/lib.rs | 52 +++++++++++++++++---------------- crates/solver_vrp/src/random.rs | 2 +- crates/solver_vrp/src/solver.rs | 2 +- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/solver_vrp/src/lib.rs b/crates/solver_vrp/src/lib.rs index 3d667a7..6de3cb1 100644 --- a/crates/solver_vrp/src/lib.rs +++ b/crates/solver_vrp/src/lib.rs @@ -2,7 +2,9 @@ //! //! A vehicle routing solver library. //! -//! This library provides functionalities to solve various vehicle routing problems (VRP), including +//! ! WARNING: Some APIs like the Plan API are still unimplemented and subject to change. +//! +//! This library provides functionalities to solve various vehicle routing problems (VRP), including: //! //! - Pickup and Delivery Problem (PDP) //! @@ -14,58 +16,58 @@ //! use solver_vrp::random::Random; //! //! // Implement a custom objective to add optimized features like linehaul costs to the model. +//! struct ZeroObjective { +//! zero: f64, +//! } +//! //! impl Objective for ZeroObjective { //! fn name(&self) -> String { -//! String::from("Zero Objective") +//! String::from("My Zero Objective") //! } //! -//! // Returns the computed value of the objective for the given plan. +//! // Computes the value of the objective for the given plan. //! fn compute(&self, _model: &Model, _solution: &Solution, _plan: &Plan) -> f64 { -//! 0.0 +//! self.zero //! } //! } //! //! // Implement a custom constraint to enforce unique business rules in the model. -//! struct MaxVehicleWeight((f64, f64)); +//! struct MyVehicleCapacities(vec![f64; 2]); //! -//! impl Constraint for MaxVehicleWeight { +//! impl Constraint for MyVehicleCapacities { //! fn name(&self) -> String { -//! String::from("Max Vehicle Weight") +//! String::from("My Vehicle Capacities") //! } //! //! // Returns true if the plan is feasible. //! fn is_feasible(&self, plan: &Plan) -> bool { -//! plan.route() -//! .changes() -//! .last() -//! .map_or(true, |change| { -//! change.capacity.utilization(1) <= self.0.1 -//! }); +//! let i = 1; +//! self.0.get(i) +//! .zip(plan.route().changes().last()) +//! .and_then(|(max, change)| change.required_capacity().get(i).map(|d| d <= max)) +//! .unwrap_or(true) //! } //! } //! //! // Add new operators to refine the solver's search and heuristic capabilities. -//! #![derive(Default)] -//! struct RejectEverything { -//! parameters: OperatorParameters, -//! }; +//! struct SimpleOperator {}; //! -//! impl Operator for CustomOperator { +//! impl Operator for SimpleOperator { //! fn name(&self) -> String { -//! String::from("Custom Operator") +//! String::from("Simple Operator") //! } //! -//! // Returns the new solution after executing the operator. +//! // Returns the new plan after executing the operator. //! fn execute(&self, _model: &Model, _solution: &Solution, _random: &mut Random) -> Plan { -//! Plan::default() +//! return Plan::new(); //! } //! } //! //! fn run() { //! // Build the model with custom components. //! let model = ModelBuilder::new() -//! .objective(CustomObjective::default()) -//! .constraint(CustomConstraint((26.0, 40_000.0))) +//! .objective(ZeroObjective { zero: 0.0 }) +//! .constraint(MyVehicleCapacities(vec![26.0, 40_000.0])) //! .build(); //! //! // Define options for the solver. @@ -77,8 +79,8 @@ //! let solver = SolverBuilder::new() //! .options(options) //! .model(model) -//! .plan(initial_solution) -//! .operator(CustomOperator {}) +//! .solution(initial_solution) +//! .operator(SimpleOperator {}) //! .build(); //! //! let best_solution = solver.solve(); diff --git a/crates/solver_vrp/src/random.rs b/crates/solver_vrp/src/random.rs index 16ca795..6a0bbe8 100644 --- a/crates/solver_vrp/src/random.rs +++ b/crates/solver_vrp/src/random.rs @@ -45,7 +45,7 @@ impl Random { } pub fn chance(&mut self, (numerator, denominator): (f64, f64)) -> bool { - if numerator == denominator { + if numerator.eq(&denominator) { return true; } self.f64() < (numerator / denominator) diff --git a/crates/solver_vrp/src/solver.rs b/crates/solver_vrp/src/solver.rs index 51645e8..8405a73 100644 --- a/crates/solver_vrp/src/solver.rs +++ b/crates/solver_vrp/src/solver.rs @@ -91,7 +91,7 @@ impl SolverBuilder { } #[must_use] - pub fn plan(mut self, solution: Solution) -> Self { + pub fn solution(mut self, solution: Solution) -> Self { self.solver.solution = Some(solution); self } From 3bfcafe259dd413c0fe16e167fb8517102d227be Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 19:24:35 -0400 Subject: [PATCH 4/7] Add defaults --- crates/solver_vrp/src/model.rs | 133 ++++++++++++++++-------------- crates/solver_vrp/src/operator.rs | 97 ++++++---------------- crates/solver_vrp/src/solver.rs | 59 +++++++------ 3 files changed, 126 insertions(+), 163 deletions(-) diff --git a/crates/solver_vrp/src/model.rs b/crates/solver_vrp/src/model.rs index 02fc6b8..0322ebe 100644 --- a/crates/solver_vrp/src/model.rs +++ b/crates/solver_vrp/src/model.rs @@ -25,7 +25,6 @@ pub trait Expression { fn compute(&self, model: &Model, solution: &Solution, plan: &Plan) -> f64; } -#[derive(Default)] pub struct Model { data: ModelData, objectives: Objectives, @@ -36,7 +35,7 @@ pub struct Model { impl Model { #[must_use] pub fn new() -> Self { - Self::default() + ModelBuilder::new().build() } #[must_use] @@ -75,6 +74,16 @@ impl Model { } } +impl Default for Model { + fn default() -> Self { + ModelBuilder::new() + .objective(UnplannedObjective {}) + .constraint(VehicleCapacityConstraint {}) + .constraint(VehicleCompatibilityConstraint {}) + .build() + } +} + #[derive(Default)] pub struct ModelData { stops: Stops, @@ -255,7 +264,7 @@ pub struct UnplannedObjective; impl Objective for UnplannedObjective { fn name(&self) -> String { - String::from("Unplanned Objective") + String::from("unplanned") } fn compute(&self, _model: &Model, _solution: &Solution, _plan: &Plan) -> f64 { @@ -263,66 +272,27 @@ impl Objective for UnplannedObjective { } } -pub enum VehicleCapacityConstraint { - MaxWeight, - MaxVolume, -} +pub struct VehicleCapacityConstraint {} -pub enum VehicleCompatibilityConstraint { - Match, -} +pub struct VehicleCompatibilityConstraint {} impl Constraint for VehicleCapacityConstraint { fn name(&self) -> String { - match self { - VehicleCapacityConstraint::MaxWeight => { - String::from("Vehicle Capacity Constraint (Max Weight)") - } - VehicleCapacityConstraint::MaxVolume => { - String::from("Vehicle Capacity Constraint (Max Volume)") - } - } + String::from("vehicle_capacity") } fn is_feasible(&self, model: &Model, solution: &Solution, plan: &Plan) -> bool { - match self { - VehicleCapacityConstraint::MaxWeight => true, - VehicleCapacityConstraint::MaxVolume => true, - } + true } } impl Constraint for VehicleCompatibilityConstraint { fn name(&self) -> String { - match self { - VehicleCompatibilityConstraint::Match => { - String::from("Vehicle Compatibility Constraint (Match)") - } - } + String::from("vehicle_compatibility") } fn is_feasible(&self, model: &Model, solution: &Solution, plan: &Plan) -> bool { - match self { - VehicleCompatibilityConstraint::Match => true, - } - } -} - -pub enum DistanceExpression { - Meters, -} - -impl Expression for DistanceExpression { - fn name(&self) -> String { - match self { - DistanceExpression::Meters => String::from("Distance Expression (Meters)"), - } - } - - fn compute(&self, _model: &Model, _solution: &Solution, _plan: &Plan) -> f64 { - match self { - DistanceExpression::Meters => 0.0, - } + true } } @@ -485,7 +455,7 @@ mod tests { } #[test] - fn test_model() { + fn test_model_build_and_access() { let stop = Stop::new(1, Location::new(1, 10.0, 20.0), vec![5.0]); let vehicle = Vehicle::new(1, vec![10.0]); let distance_matrix = DistanceMatrix::new(vec![vec![0.0, 1.0], vec![1.0, 0.0]]); @@ -494,46 +464,87 @@ mod tests { .stop(stop) .vehicle(vehicle) .distance_matrix(distance_matrix) - .objective(UnplannedObjective) - .objective(TestObjective) - .constraint(VehicleCapacityConstraint::MaxVolume) - .constraint(VehicleCompatibilityConstraint::Match) - .constraint(TestConstraint) - .expression(DistanceExpression::Meters) - .expression(TestExpression) + .objective(UnplannedObjective {}) + .objective(TestObjective {}) + .constraint(VehicleCapacityConstraint {}) + .constraint(VehicleCompatibilityConstraint {}) + .constraint(TestConstraint {}) + .expression(TestExpression {}) .build(); assert_eq!(model.stops().len(), 1); assert_eq!(model.vehicles().len(), 1); assert!(model.distance_matrix().is_some()); + } + #[test] + fn test_model_objective_count() { + let model = ModelBuilder::new() + .objective(UnplannedObjective) + .objective(TestObjective) + .build(); assert_eq!(model.objectives().len(), 2); + } + + #[test] + fn test_model_constraint_count() { + let model = ModelBuilder::new() + .constraint(VehicleCapacityConstraint {}) + .constraint(VehicleCompatibilityConstraint {}) + .constraint(TestConstraint {}) + .build(); assert_eq!(model.constraints().len(), 3); - assert_eq!(model.expressions().len(), 2); + } + + #[test] + fn test_model_expression_count() { + let model = ModelBuilder::new().expression(TestExpression).build(); + assert_eq!(model.expressions().len(), 1); + } + #[test] + fn test_model_objective_names() { + let model = ModelBuilder::new() + .objective(UnplannedObjective) + .objective(TestObjective) + .build(); assert_eq!( model.objectives().first().map(|o| o.name()), - Some(String::from("Unplanned Objective")) + Some(String::from("unplanned")) ); assert_eq!( model.objectives().get(1).map(|o| o.name()), Some(String::from("Test Objective")) ); + } + + #[test] + fn test_model_constraint_names() { + let model = ModelBuilder::new() + .constraint(VehicleCapacityConstraint {}) + .constraint(VehicleCompatibilityConstraint {}) + .constraint(TestConstraint {}) + .build(); assert_eq!( model.constraints().first().map(|c| c.name()), - Some(String::from("Vehicle Capacity Constraint (Max Volume)")) + Some(String::from("vehicle_capacity")) ); assert_eq!( model.constraints().get(1).map(|c| c.name()), - Some(String::from("Vehicle Compatibility Constraint (Match)")) + Some(String::from("vehicle_compatibility")) ); assert_eq!( model.constraints().get(2).map(|c| c.name()), Some(String::from("Test Constraint")) ); + } + + #[test] + fn test_model_expression_names() { + let model = ModelBuilder::new().expression(TestExpression).build(); assert_eq!( model.expressions().first().map(|e| e.name()), - Some(String::from("Distance Expression (Meters)")) + Some(String::from("Test Expression")) ); } diff --git a/crates/solver_vrp/src/operator.rs b/crates/solver_vrp/src/operator.rs index 655b04a..175b98c 100644 --- a/crates/solver_vrp/src/operator.rs +++ b/crates/solver_vrp/src/operator.rs @@ -42,84 +42,55 @@ impl Operators { } } -pub enum RepairOperator { - Random(OperatorParameters), - Nearest(OperatorParameters), +pub struct RepairOperator { + pub parameters: OperatorParameters, +} + +impl Default for RepairOperator { + fn default() -> Self { + Self { + parameters: OperatorParameters::new(1.0, 1.0), + } + } } impl Operator for RepairOperator { fn name(&self) -> String { - match self { - Self::Random(_) => String::from("Repair Operator (Random)"), - Self::Nearest(_) => String::from("Repair Operator (Nearest)"), - } + String::from("repair") } fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { - match self { - Self::Random(p) => repair_random(model, solution, p, random), - Self::Nearest(p) => repair_nearest(model, solution, p, random), - } + repair_nearest(model, solution, &self.parameters, random) } fn chance(&self) -> f64 { - match self { - RepairOperator::Random(p) | RepairOperator::Nearest(p) => p.chance_f64, - } + self.parameters.chance_f64 } } -pub enum DestroyOperator { - Random(OperatorParameters), - Nearest(OperatorParameters), +pub struct DestroyOperator { + pub parameters: OperatorParameters, } -impl Operator for DestroyOperator { - fn name(&self) -> String { - match self { - Self::Random(_) => String::from("Destroy Operator (Random)"), - Self::Nearest(_) => String::from("Destroy Operator (Nearest)"), - } - } - - fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { - match self { - Self::Random(p) => destroy_random(model, solution, p, random), - Self::Nearest(p) => destroy_nearest(model, solution, p, random), - } - } - - fn chance(&self) -> f64 { - match self { - DestroyOperator::Random(p) | DestroyOperator::Nearest(p) => p.chance_f64, +impl Default for DestroyOperator { + fn default() -> Self { + Self { + parameters: OperatorParameters::new(1.0, 1.0), } } } -pub enum ResetOperator { - Full(OperatorParameters), - Partial(OperatorParameters), -} - -impl Operator for ResetOperator { +impl Operator for DestroyOperator { fn name(&self) -> String { - match self { - Self::Full(_) => String::from("Reset Operator (Full)"), - Self::Partial(_) => String::from("Reset Operator (Partial)"), - } + String::from("destroy") } fn execute(&self, model: &Model, solution: &Solution, random: &mut Random) -> Plan { - match self { - Self::Full(p) => reset_full(model, solution, p, random), - Self::Partial(p) => reset_partial(model, solution, p, random), - } + destroy_random(model, solution, &self.parameters, random) } fn chance(&self) -> f64 { - match self { - ResetOperator::Full(p) | ResetOperator::Partial(p) => p.chance_f64, - } + self.parameters.chance_f64 } } @@ -159,27 +130,9 @@ fn destroy_nearest( todo!() } -fn reset_full( - _model: &Model, - _solution: &Solution, - _params: &OperatorParameters, - _random: &mut Random, -) -> Plan { - todo!() -} - -fn reset_partial( - _model: &Model, - _solution: &Solution, - _params: &OperatorParameters, - _random: &mut Random, -) -> Plan { - todo!() -} - pub struct OperatorParameters { - value: f64, - chance_f64: f64, + pub value: f64, + pub chance_f64: f64, } impl OperatorParameters { diff --git a/crates/solver_vrp/src/solver.rs b/crates/solver_vrp/src/solver.rs index 8405a73..e5d2953 100644 --- a/crates/solver_vrp/src/solver.rs +++ b/crates/solver_vrp/src/solver.rs @@ -1,9 +1,8 @@ use crate::model::Model; -use crate::operator::{Operator, Operators}; +use crate::operator::{DestroyOperator, Operator, OperatorParameters, Operators, RepairOperator}; use crate::random::Random; use crate::solution::Solution; -#[derive(Default)] pub struct Solver { model: Model, operators: Operators, @@ -14,6 +13,18 @@ pub struct Solver { } impl Solver { + #[must_use] + pub fn new() -> Self { + Self { + model: Model::new(), + operators: Operators::new(), + options: SolverOptions::default(), + solution: None, + random: Random::new(), + iteration_count: 0, + } + } + #[must_use] pub fn model(&self) -> &Model { &self.model @@ -61,6 +72,17 @@ impl Solver { } } +impl Default for Solver { + fn default() -> Self { + SolverBuilder::new() + .model(Model::default()) + .operator(DestroyOperator::default()) + .operator(RepairOperator::default()) + .options(SolverOptions::default()) + .build() + } +} + #[derive(Default)] pub struct SolverBuilder { solver: Solver, @@ -69,7 +91,9 @@ pub struct SolverBuilder { impl SolverBuilder { #[must_use] pub fn new() -> Self { - Self::default() + Self { + solver: Solver::new(), + } } #[must_use] @@ -123,40 +147,15 @@ impl Default for SolverOptions { #[cfg(test)] mod tests { - use crate::{ - model::{ - DistanceExpression, ModelBuilder, UnplannedObjective, VehicleCapacityConstraint, - VehicleCompatibilityConstraint, - }, - operator::{DestroyOperator, OperatorParameters, RepairOperator, ResetOperator}, - }; - use super::*; #[test] fn test_solver() { let options = SolverOptions::new(10); - - let model = ModelBuilder::new() - .objective(UnplannedObjective) - .constraint(VehicleCapacityConstraint::MaxVolume) - .constraint(VehicleCompatibilityConstraint::Match) - .expression(DistanceExpression::Meters) - .build(); - - let solver = SolverBuilder::new() - .operator(RepairOperator::Random(OperatorParameters::new(1.0, 0.5))) - .operator(RepairOperator::Nearest(OperatorParameters::new(1.0, 0.5))) - .operator(DestroyOperator::Random(OperatorParameters::new(2.0, 0.3))) - .operator(DestroyOperator::Nearest(OperatorParameters::new(2.0, 0.3))) - .operator(ResetOperator::Partial(OperatorParameters::new(3.0, 0.2))) - .operator(ResetOperator::Full(OperatorParameters::new(3.0, 0.2))) - .options(options) - .model(model) - .build(); + let solver = SolverBuilder::default().options(options).build(); assert_eq!(solver.options.max_iterations, 10); - assert_eq!(solver.operators().len(), 6); + assert_eq!(solver.operators().len(), 2); assert_eq!(solver.iteration_count, 0); assert!(solver.solution.is_none()); assert_eq!(solver.model.objectives().len(), 1); From db444d380e1d759ac95344c0ea651cd31abbbbf7 Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 19:24:54 -0400 Subject: [PATCH 5/7] Clippy --- crates/solver_vrp/src/solver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/solver_vrp/src/solver.rs b/crates/solver_vrp/src/solver.rs index e5d2953..bd4e079 100644 --- a/crates/solver_vrp/src/solver.rs +++ b/crates/solver_vrp/src/solver.rs @@ -1,5 +1,5 @@ use crate::model::Model; -use crate::operator::{DestroyOperator, Operator, OperatorParameters, Operators, RepairOperator}; +use crate::operator::{DestroyOperator, Operator, Operators, RepairOperator}; use crate::random::Random; use crate::solution::Solution; From 1fa67465b7dd3ad8780550893879c7f34d80d3f9 Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 19:26:09 -0400 Subject: [PATCH 6/7] Simpler CI --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8bd03ef..d2a289c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly @@ -28,7 +28,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly From bf0ff071a84dc779de63e6d1f30d265c50f64285 Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Mon, 27 Oct 2025 19:30:31 -0400 Subject: [PATCH 7/7] Chore --- crates/solver_vrp/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/solver_vrp/src/lib.rs b/crates/solver_vrp/src/lib.rs index 6de3cb1..0d33386 100644 --- a/crates/solver_vrp/src/lib.rs +++ b/crates/solver_vrp/src/lib.rs @@ -4,7 +4,7 @@ //! //! ! WARNING: Some APIs like the Plan API are still unimplemented and subject to change. //! -//! This library provides functionalities to solve various vehicle routing problems (VRP), including: +//! Supported: //! //! - Pickup and Delivery Problem (PDP) //! @@ -44,7 +44,7 @@ //! let i = 1; //! self.0.get(i) //! .zip(plan.route().changes().last()) -//! .and_then(|(max, change)| change.required_capacity().get(i).map(|d| d <= max)) +//! .and_then(|(max, change)| change.requirements().capacity().get(i).map(|r| r <= max)) //! .unwrap_or(true) //! } //! }