From 630a27954ccde919f4f427efb90ca26db4f02b0e Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Fri, 13 Sep 2024 16:20:11 -0400 Subject: [PATCH 01/14] Add bertsekas algo for assignment problem --- src/bertsekas.rs | 377 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 378 insertions(+) create mode 100644 src/bertsekas.rs diff --git a/src/bertsekas.rs b/src/bertsekas.rs new file mode 100644 index 00000000..db3ec193 --- /dev/null +++ b/src/bertsekas.rs @@ -0,0 +1,377 @@ +//! Bertekas Auction Algorithm for the Assignment Problem +//! +use std::sync::mpsc::{channel, Receiver, Sender}; + +/// A simple data structure that keeps track of all data required to +/// assign agents to tasks. +pub struct Auction { + cost_matrix: Vec>, + assignments: Vec>, + prices: Vec, + epsilon: f64, + /// This is a mapping of every single possible task and the corresponding set of bids that + /// each agent made for that task. Thus, each task, j, has a list of (agent, bid) tuples + /// that were put in by each agent, i. + task_bids: Vec>, +} + +impl Auction { + /// Returns a new [`Auction`] based on the cost matrix used to determine optimal assignments. + pub fn new(cost_matrix: Vec>) -> Self { + let m = cost_matrix.len(); + let n = cost_matrix[0].len(); + + let prices = vec![0.0; n]; + let assignments = vec![None; m]; + let task_bids = vec![Vec::with_capacity(m); n]; + + let epsilon = 1.0 / (n + 1) as f64; + + Self { + cost_matrix, + assignments, + prices, + epsilon, + task_bids, + } + } + + /// Compute the score after assigning all agents to tasks + pub fn score(&self) -> Option { + let mut res = 0.0; + + if self.all_assigned() { + for (i, assigned_task) in self.assignments.iter().enumerate() { + if let Some(j) = *assigned_task { + res += self.cost_matrix[i][j]; + } + } + Some(res) + } else { + None + } + } + + pub(crate) fn scale_epsilon(&mut self) { + self.epsilon *= 2.0; + } + + pub(crate) fn update_price(&mut self, task: usize, price: f64) { + self.prices[task] = price; + } + + /// The number of agents (i.e., the # of rows in the cost matrix.) + pub fn num_agents(&self) -> usize { + self.cost_matrix.len() + } + + /// The number of tasks (i.e., the # of cols in the cost matrix.) + + pub fn num_tasks(&self) -> usize { + self.cost_matrix[0].len() + } + + fn num_assigned(&self) -> usize { + self.assignments.iter().filter(|&&x| x.is_some()).count() + } + + fn assign(&mut self, agent: usize, task: usize) { + for (i, a) in self.assignments.iter_mut().enumerate() { + if i != agent && *a == Some(task) { + *a = None; + } + } + self.assignments[agent] = Some(task); + } + + /// Check if all agents were assigned. There are 3 possible cases to consider: + /// + /// 1. `# agents < # tasks` + /// + /// In this case, we know once all agents are assigned to a subset of ojects, + /// all agents are assigned. + /// + /// 2. `# agents == # tasks` + /// + /// This is the simplest case and *should* be optimal. That is, + /// we have a bijection between agents and tasks + /// + /// 3. `# agents > # tasks` + /// + /// This is the case where the number of possible agents assigned + /// is always going to be less than the number of possible ojbects. + /// Let m be the number of agents, and let n be the number of tasks, + /// then we will always have `k = m - n` agents that can be assigned. + pub fn all_assigned(&self) -> bool { + if self.num_agents() > self.num_tasks() { + // Case 3: More agents than tasks. We should have exactly `n` agents assigned. + self.num_assigned() == self.num_tasks() + } else { + // Case 1 & 2: Less or equal agents than tasks. We should have all agents assigned. + self.num_assigned() == self.num_agents() + } + } + + /// Should this be public? + pub fn is_unassigned(&self, agent: usize) -> bool { + self.assignments[agent].is_none() + } + + fn add_task_bid(&mut self, agent: usize, task: usize, bid: f64) { + self.task_bids[task].push((agent, bid)); + } + + /// We need to clear out all the bids that each agent made for each task + fn clear_task_bids(&mut self) { + for bids in &mut self.task_bids { + bids.clear(); + } + } +} + +/// Tuple struct of (agent, task, bid) +struct Bid(usize, usize, f64); + +impl Bid { + pub fn new(agent: usize, task: usize, bid: f64) -> Self { + Self(agent, task, bid) + } +} + +fn bid(agent_row: &[f64], prices: &[f64], epsilon: f64, unassigned_agent: usize, tx: &Sender) { + let mut best_task = None; + let mut best_profit = f64::NEG_INFINITY; + let mut next_best_profit = f64::NEG_INFINITY; + + for ((j, val), price_j) in agent_row.iter().enumerate().zip(prices.iter()) { + // deferencing these first makes the flamegraph take + // less time on Sub<&f64> + let profit = (*val) - (*price_j); + + if profit > best_profit { + next_best_profit = best_profit; + best_profit = profit; + best_task = Some(j); + } else if profit > next_best_profit { + next_best_profit = profit; + } + } + + if let Some(best_obj) = best_task { + let bid_value = prices[best_obj] + best_profit - next_best_profit + epsilon; + let bid_for_agent = Bid::new(unassigned_agent, best_obj, bid_value); + tx.send(bid_for_agent).unwrap(); + } +} + +/// This is known as the "Jacobi bidding" version. +/// Essentially, all agents bid for tasks, and only then +/// do we make an assignment. +fn bid_phase(auction_data: &mut Auction) { + let (tx, rx): (Sender, Receiver) = channel(); + + let mut num_bids = 0; + for p in 0..auction_data.num_agents() { + if auction_data.is_unassigned(p) { + num_bids += 1; + } + } + + for p in 0..auction_data.num_agents() { + if auction_data.is_unassigned(p) { + let agent_row = &auction_data.cost_matrix[p]; + let prices = &auction_data.prices; + let eps = auction_data.epsilon; + bid(agent_row, prices, eps, p, &tx); + } + } + + // println!("waiting to assign bids for tasks..."); + for _ in 0..num_bids { + let bid = rx.recv().unwrap(); + + // auction_data.add_task_bid(unassigned_agent, best_obj, bid_value); + auction_data.add_task_bid(bid.0, bid.1, bid.2); + } + // println!("bidding phase complete"); +} + +fn assignment_phase(auction_data: &mut Auction) { + let (tx, rx): (Sender>, Receiver>) = channel(); + + let mut num_tasks = 0; + for _ in auction_data.task_bids.iter() { + num_tasks += 1; + } + + for (task, bids) in auction_data.task_bids.iter().enumerate() { + let mut max_bid = f64::NEG_INFINITY; + let mut bid_winner = None; + + for b in bids.iter() { + let (agent, agents_bid) = *b; + if agents_bid > max_bid { + max_bid = agents_bid; + bid_winner = Some(agent); + } + } + + if let Some(bw) = bid_winner { + tx.send(Some(Bid::new(bw, task, max_bid))).unwrap(); + } else { + tx.send(None).unwrap(); + } + } + // println!("sent all bids via tx in assignment phase"); + + for _i in 0..num_tasks { + if let Some(Bid(bid_winner, task, max_bid)) = rx.recv().unwrap() { + auction_data.update_price(task, max_bid); + auction_data.assign(bid_winner, task); + } + } + + auction_data.clear_task_bids(); // Clear bids after each assignment phase + // println!("assignment phase complete"); +} + +/// Run the forward auction only. The way to consider this +/// is that agents are going to bid for tasks. Agents will +/// be assigned to a task after each bidding phase. +pub fn forward(auction_data: &mut Auction) { + while !auction_data.all_assigned() { + bid_phase(auction_data); + assignment_phase(auction_data); + auction_data.scale_epsilon(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{kuhn_munkres::kuhn_munkres, matrix}; + use rand::Rng; + + #[test] + fn basic_functionality_maximization() { + let matrix = vec![ + vec![1.0, 2.0, 20.0, 2.5], + vec![7.0, 5.0, 11.0, 3.0], + vec![6.0, 1.0, 1.5, 12.0], + vec![0.0, 14.0, 3.7, 14.0], + ]; + + let mut auction_data = Auction::new(matrix); + forward(&mut auction_data); + + let expected_assignments = vec![Some(2), Some(0), Some(3), Some(1)]; + assert_eq!(auction_data.assignments, expected_assignments); + + let score = auction_data.score(); + assert!(score.is_some()); + println!("{}", score.unwrap()); + } + + #[test] + fn all_high_values() { + let matrix = vec![ + vec![1000.0, 1000.0, 1000.0, 1000.0], + vec![1000.0, 1000.0, 1000.0, 1000.0], + vec![1000.0, 1000.0, 1000.0, 1000.0], + vec![1000.0, 1000.0, 1000.0, 1000.0], + ]; + + let mut auction_data = Auction::new(matrix); + forward(&mut auction_data); + + // Any assignment is optimal since all profits are equal. + assert!(auction_data.all_assigned()); + assert_eq!( + auction_data + .assignments + .iter() + .filter(|x| x.is_some()) + .count(), + 4 + ); + } + + #[test] + fn rectangular_matrix_more_agents_maximization() { + let matrix = vec![ + vec![10.0, 15.0, 20.0], + vec![5.0, 30.0, 25.0], + vec![35.0, 10.0, 15.0], + vec![10.0, 20.0, 25.0], + ]; + + let mut auction_data = Auction::new(matrix); + forward(&mut auction_data); + + assert!(auction_data.all_assigned()); + assert_eq!( + auction_data + .assignments + .iter() + .filter(|x| x.is_some()) + .count(), + 3 + ); + } + + #[test] + fn single_agent_single_task() { + let matrix = vec![vec![42.0]]; + + let mut auction_data = Auction::new(matrix); + forward(&mut auction_data); + + // The only assignment possible should be agent 0 + let expected_assignments = vec![Some(0)]; + assert_eq!(auction_data.assignments, expected_assignments); + } + + #[test] + fn large_matrix() { + let m = 700; + let matrix: Vec> = (0..m) + .map(|i| (0..m).map(|j| (i + j) as f64).collect()) + .collect(); + + let mut auction_data = Auction::new(matrix); + forward(&mut auction_data); + + assert!(auction_data.all_assigned()); + assert_eq!(auction_data.num_assigned(), m); + } + + #[test] + fn compare_with_pathfinding_basic_functionality() { + let rows = 200; + let cols = 200; + let mut rng = rand::thread_rng(); + let matrix: Vec = (0..rows * cols).map(|_| rng.gen_range(0..100)).collect(); + let matrix = matrix::Matrix::from_vec(rows, cols, matrix).unwrap(); + + let mut cost_matrix = Vec::new(); + for r in matrix.iter() { + cost_matrix.push(r.iter().map(|a| *a as f64).collect()); + } + + let now = std::time::Instant::now(); + let mut auction_data = Auction::new(cost_matrix); + forward(&mut auction_data); + let elapsed = now.elapsed().as_micros(); + println!("bertekas auction complete in {elapsed}"); + println!("score: {}", auction_data.score().unwrap()); + println!("assignments: {:?}\n", auction_data.assignments); + + // Run Munkres algorithm using pathfinding crate + let now = std::time::Instant::now(); + let (score, assignments) = kuhn_munkres(&matrix); + let elapsed = now.elapsed().as_micros(); + println!("hungarian algo complete in {elapsed}"); + println!("hungarian algo score: {score}"); + println!("hungarian algo assignments: {assignments:?}"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 62183b81..7949db70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ use deprecate_until::deprecate_until; pub use num_traits; +pub mod bertsekas; pub mod directed; pub mod grid; pub mod kuhn_munkres; From b9092c3c387733a8874c8258e5c2c1224a3be168 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sun, 15 Sep 2024 20:24:07 -0400 Subject: [PATCH 02/14] Utilize `Matrix` as cost matrix and add benchmark --- Cargo.toml | 8 +- benches/kuhn_munkres_vs_bertsekas.rs | 49 ++++++++ src/bertsekas.rs | 180 ++++++++++++++++----------- src/matrix.rs | 10 ++ 4 files changed, 170 insertions(+), 77 deletions(-) create mode 100644 benches/kuhn_munkres_vs_bertsekas.rs diff --git a/Cargo.toml b/Cargo.toml index 4e3ee124..4527def4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ rust-version = "1.77.2" sign-commit = true sign-tag = true pre-release-replacements = [ - {file = "README.md", search = "pathfinding = \".*\"", replace = "pathfinding = \"{{version}}\"", exactly = 1}, - {file = "CHANGELOG.md", search = "n\\.n\\.n", replace = "{{tag_name}}", exactly = 1} + { file = "README.md", search = "pathfinding = \".*\"", replace = "pathfinding = \"{{version}}\"", exactly = 1 }, + { file = "CHANGELOG.md", search = "n\\.n\\.n", replace = "{{tag_name}}", exactly = 1 }, ] [dependencies] @@ -68,6 +68,10 @@ harness = false name = "kuhn_munkres" harness = false +[[bench]] +name = "kuhn_munkres_vs_bertsekas" +harness = false + [[bench]] name = "separate_components" harness = false diff --git a/benches/kuhn_munkres_vs_bertsekas.rs b/benches/kuhn_munkres_vs_bertsekas.rs new file mode 100644 index 00000000..30df16ff --- /dev/null +++ b/benches/kuhn_munkres_vs_bertsekas.rs @@ -0,0 +1,49 @@ +use codspeed_criterion_compat::Throughput; +use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use pathfinding::bertsekas::{forward, Auction}; +use pathfinding::prelude::{kuhn_munkres, Matrix}; +use rand::Rng; + +fn create_matrices(size: usize) -> (Matrix, Matrix) { + let mut rng = rand::thread_rng(); + let int_matrix: Matrix = Matrix::from_fn(size, size, |_| rng.gen_range(0..100)); + let float_matrix = int_matrix.clone().map(|value| value as f64); + (int_matrix, float_matrix) +} + +fn compare_algorithms(c: &mut Criterion) { + let mut group = c.benchmark_group("Assignment Problem"); + + let sizes = [10, 20, 50, 100, 200, 500, 1000]; + + for size in sizes.iter() { + // Bertekas Auction - Time + group.bench_with_input( + BenchmarkId::new("Bertekas Auction Time", size), + size, + |b, &size| { + let (_, float_matrix) = create_matrices(size); + let mut auction_data = Auction::new(float_matrix); + b.iter(|| { + forward(&mut auction_data); + }); + }, + ); + + // Hungarian Algorithm - Time + group.bench_with_input( + BenchmarkId::new("Hungarian Algorithm Time", size), + size, + |b, &size| { + let (int_64matrix, _) = create_matrices(size); + b.iter(|| { + kuhn_munkres(&int_64matrix); + }); + }, + ); + } + group.finish(); +} + +criterion_group!(benches, compare_algorithms); +criterion_main!(benches); diff --git a/src/bertsekas.rs b/src/bertsekas.rs index db3ec193..fc75b923 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -1,31 +1,37 @@ //! Bertekas Auction Algorithm for the Assignment Problem -//! +use num_traits::FloatConst; + +use crate::{matrix::Matrix, prelude::Weights}; use std::sync::mpsc::{channel, Receiver, Sender}; /// A simple data structure that keeps track of all data required to /// assign agents to tasks. -pub struct Auction { - cost_matrix: Vec>, +pub struct Auction { + cost_matrix: Matrix, assignments: Vec>, - prices: Vec, - epsilon: f64, + prices: Vec, + epsilon: T, /// This is a mapping of every single possible task and the corresponding set of bids that /// each agent made for that task. Thus, each task, j, has a list of (agent, bid) tuples /// that were put in by each agent, i. - task_bids: Vec>, + task_bids: Vec>, } -impl Auction { +impl Auction +where + T: num_traits::Float, +{ /// Returns a new [`Auction`] based on the cost matrix used to determine optimal assignments. - pub fn new(cost_matrix: Vec>) -> Self { - let m = cost_matrix.len(); - let n = cost_matrix[0].len(); + #[must_use] + pub fn new(cost_matrix: Matrix) -> Self { + let m = cost_matrix.rows; + let n = cost_matrix.columns; - let prices = vec![0.0; n]; + let prices = vec![T::zero(); n]; let assignments = vec![None; m]; let task_bids = vec![Vec::with_capacity(m); n]; - let epsilon = 1.0 / (n + 1) as f64; + let epsilon = T::from(n + 1).unwrap().recip(); Self { cost_matrix, @@ -37,13 +43,17 @@ impl Auction { } /// Compute the score after assigning all agents to tasks - pub fn score(&self) -> Option { - let mut res = 0.0; + #[must_use] + pub fn score(&self) -> Option + where + T: num_traits::Float + std::ops::AddAssign, + { + let mut res: T = T::zero(); if self.all_assigned() { for (i, assigned_task) in self.assignments.iter().enumerate() { if let Some(j) = *assigned_task { - res += self.cost_matrix[i][j]; + res += *self.cost_matrix.get((i, j)).unwrap(); } } Some(res) @@ -52,23 +62,27 @@ impl Auction { } } - pub(crate) fn scale_epsilon(&mut self) { - self.epsilon *= 2.0; + pub(crate) fn scale_epsilon(&mut self) + where + T: num_traits::Float + num_traits::FloatConst + std::ops::MulAssign, + { + self.epsilon *= T::from(1.01).unwrap(); } - pub(crate) fn update_price(&mut self, task: usize, price: f64) { + pub(crate) fn update_price(&mut self, task: usize, price: T) { self.prices[task] = price; } /// The number of agents (i.e., the # of rows in the cost matrix.) + #[must_use] pub fn num_agents(&self) -> usize { - self.cost_matrix.len() + self.cost_matrix.rows() } /// The number of tasks (i.e., the # of cols in the cost matrix.) - + #[must_use] pub fn num_tasks(&self) -> usize { - self.cost_matrix[0].len() + self.cost_matrix.columns() } fn num_assigned(&self) -> usize { @@ -102,6 +116,7 @@ impl Auction { /// is always going to be less than the number of possible ojbects. /// Let m be the number of agents, and let n be the number of tasks, /// then we will always have `k = m - n` agents that can be assigned. + #[must_use] pub fn all_assigned(&self) -> bool { if self.num_agents() > self.num_tasks() { // Case 3: More agents than tasks. We should have exactly `n` agents assigned. @@ -113,11 +128,15 @@ impl Auction { } /// Should this be public? + #[must_use] pub fn is_unassigned(&self, agent: usize) -> bool { self.assignments[agent].is_none() } - fn add_task_bid(&mut self, agent: usize, task: usize, bid: f64) { + fn add_task_bid(&mut self, agent: usize, task: usize, bid: T) + where + T: num_traits::Float, + { self.task_bids[task].push((agent, bid)); } @@ -130,22 +149,34 @@ impl Auction { } /// Tuple struct of (agent, task, bid) -struct Bid(usize, usize, f64); +struct Bid { + agent: usize, + task: usize, + amount: T, +} -impl Bid { - pub fn new(agent: usize, task: usize, bid: f64) -> Self { - Self(agent, task, bid) +impl Bid { + pub fn new(agent: usize, task: usize, amount: T) -> Self + where + T: num_traits::Float + num_traits::FloatConst, + { + Self { + agent, + task, + amount, + } } } -fn bid(agent_row: &[f64], prices: &[f64], epsilon: f64, unassigned_agent: usize, tx: &Sender) { +fn bid(agent_row: &[T], prices: &[T], epsilon: T, unassigned_agent: usize, tx: &Sender>) +where + T: num_traits::Float + num_traits::FloatConst, +{ let mut best_task = None; - let mut best_profit = f64::NEG_INFINITY; - let mut next_best_profit = f64::NEG_INFINITY; + let mut best_profit = T::neg_infinity(); + let mut next_best_profit = T::neg_infinity(); for ((j, val), price_j) in agent_row.iter().enumerate().zip(prices.iter()) { - // deferencing these first makes the flamegraph take - // less time on Sub<&f64> let profit = (*val) - (*price_j); if profit > best_profit { @@ -167,8 +198,11 @@ fn bid(agent_row: &[f64], prices: &[f64], epsilon: f64, unassigned_agent: usize, /// This is known as the "Jacobi bidding" version. /// Essentially, all agents bid for tasks, and only then /// do we make an assignment. -fn bid_phase(auction_data: &mut Auction) { - let (tx, rx): (Sender, Receiver) = channel(); +fn bid_phase(auction_data: &mut Auction) +where + T: num_traits::Float + num_traits::FloatConst, +{ + let (tx, rx): (Sender>, Receiver>) = channel(); let mut num_bids = 0; for p in 0..auction_data.num_agents() { @@ -179,7 +213,7 @@ fn bid_phase(auction_data: &mut Auction) { for p in 0..auction_data.num_agents() { if auction_data.is_unassigned(p) { - let agent_row = &auction_data.cost_matrix[p]; + let agent_row = &auction_data.cost_matrix.get_row(p).unwrap(); let prices = &auction_data.prices; let eps = auction_data.epsilon; bid(agent_row, prices, eps, p, &tx); @@ -191,24 +225,27 @@ fn bid_phase(auction_data: &mut Auction) { let bid = rx.recv().unwrap(); // auction_data.add_task_bid(unassigned_agent, best_obj, bid_value); - auction_data.add_task_bid(bid.0, bid.1, bid.2); + auction_data.add_task_bid(bid.agent, bid.task, bid.amount); } // println!("bidding phase complete"); } -fn assignment_phase(auction_data: &mut Auction) { - let (tx, rx): (Sender>, Receiver>) = channel(); +fn assignment_phase(auction_data: &mut Auction) +where + T: num_traits::Float + num_traits::FloatConst, +{ + let (tx, rx): (Sender>>, Receiver>>) = channel(); let mut num_tasks = 0; - for _ in auction_data.task_bids.iter() { + for _ in &auction_data.task_bids { num_tasks += 1; } for (task, bids) in auction_data.task_bids.iter().enumerate() { - let mut max_bid = f64::NEG_INFINITY; + let mut max_bid = T::neg_infinity(); let mut bid_winner = None; - for b in bids.iter() { + for b in bids { let (agent, agents_bid) = *b; if agents_bid > max_bid { max_bid = agents_bid; @@ -225,7 +262,12 @@ fn assignment_phase(auction_data: &mut Auction) { // println!("sent all bids via tx in assignment phase"); for _i in 0..num_tasks { - if let Some(Bid(bid_winner, task, max_bid)) = rx.recv().unwrap() { + if let Some(Bid { + agent: bid_winner, + task, + amount: max_bid, + }) = rx.recv().unwrap() + { auction_data.update_price(task, max_bid); auction_data.assign(bid_winner, task); } @@ -238,7 +280,10 @@ fn assignment_phase(auction_data: &mut Auction) { /// Run the forward auction only. The way to consider this /// is that agents are going to bid for tasks. Agents will /// be assigned to a task after each bidding phase. -pub fn forward(auction_data: &mut Auction) { +pub fn forward(auction_data: &mut Auction) +where + T: num_traits::Float + num_traits::FloatConst + std::ops::MulAssign, +{ while !auction_data.all_assigned() { bid_phase(auction_data); assignment_phase(auction_data); @@ -260,6 +305,7 @@ mod tests { vec![6.0, 1.0, 1.5, 12.0], vec![0.0, 14.0, 3.7, 14.0], ]; + let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(matrix); forward(&mut auction_data); @@ -281,6 +327,7 @@ mod tests { vec![1000.0, 1000.0, 1000.0, 1000.0], ]; + let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(matrix); forward(&mut auction_data); @@ -297,46 +344,30 @@ mod tests { } #[test] - fn rectangular_matrix_more_agents_maximization() { - let matrix = vec![ - vec![10.0, 15.0, 20.0], - vec![5.0, 30.0, 25.0], - vec![35.0, 10.0, 15.0], - vec![10.0, 20.0, 25.0], - ]; + fn single_agent_single_task() { + let matrix = vec![vec![42.0]]; + let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(matrix); forward(&mut auction_data); - assert!(auction_data.all_assigned()); - assert_eq!( - auction_data - .assignments - .iter() - .filter(|x| x.is_some()) - .count(), - 3 - ); + // The only assignment possible should be agent 0 + let expected_assignments = vec![Some(0)]; + assert_eq!(auction_data.assignments, expected_assignments); } #[test] - fn single_agent_single_task() { - let matrix = vec![vec![42.0]]; + fn empty() { + let matrix: Matrix = Matrix::new_empty(0); let mut auction_data = Auction::new(matrix); forward(&mut auction_data); - - // The only assignment possible should be agent 0 - let expected_assignments = vec![Some(0)]; - assert_eq!(auction_data.assignments, expected_assignments); } #[test] fn large_matrix() { let m = 700; - let matrix: Vec> = (0..m) - .map(|i| (0..m).map(|j| (i + j) as f64).collect()) - .collect(); + let matrix = Matrix::from_fn(m, m, |(i, j)| (i + j) as f64); let mut auction_data = Auction::new(matrix); forward(&mut auction_data); @@ -350,16 +381,15 @@ mod tests { let rows = 200; let cols = 200; let mut rng = rand::thread_rng(); - let matrix: Vec = (0..rows * cols).map(|_| rng.gen_range(0..100)).collect(); - let matrix = matrix::Matrix::from_vec(rows, cols, matrix).unwrap(); - let mut cost_matrix = Vec::new(); - for r in matrix.iter() { - cost_matrix.push(r.iter().map(|a| *a as f64).collect()); - } + // Create the integer matrix + let int_matrix: Matrix = Matrix::from_fn(rows, cols, |_| rng.gen_range(0..100)); + + // Create the float matrix as a clone of the integer matrix + let float_matrix = int_matrix.clone().map(|value| value as f64); let now = std::time::Instant::now(); - let mut auction_data = Auction::new(cost_matrix); + let mut auction_data = Auction::new(float_matrix); forward(&mut auction_data); let elapsed = now.elapsed().as_micros(); println!("bertekas auction complete in {elapsed}"); @@ -368,7 +398,7 @@ mod tests { // Run Munkres algorithm using pathfinding crate let now = std::time::Instant::now(); - let (score, assignments) = kuhn_munkres(&matrix); + let (score, assignments) = kuhn_munkres(&int_matrix); let elapsed = now.elapsed().as_micros(); println!("hungarian algo complete in {elapsed}"); println!("hungarian algo score: {score}"); diff --git a/src/matrix.rs b/src/matrix.rs index 61215267..61c0a2f9 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -451,6 +451,16 @@ impl Matrix { }) } + /// Access a reference to a row as a slice. + #[must_use] + pub fn get_row(&self, row: usize) -> Option<&[C]> { + if row < self.rows { + Some(&self.data[row * self.columns..(row + 1) * self.columns]) + } else { + None + } + } + /// Flip the matrix around the vertical axis. pub fn flip_lr(&mut self) { for r in 0..self.rows { From 2f3b7e413eb5eb7e5ae9df8a7269268fff7e6a6a Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Sun, 15 Sep 2024 23:09:07 -0400 Subject: [PATCH 03/14] Add lifetime to avoid cloning cost matrix --- benches/kuhn_munkres_vs_bertsekas.rs | 54 ++++++++++++++-------------- src/bertsekas.rs | 49 +++++++++++++++---------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/benches/kuhn_munkres_vs_bertsekas.rs b/benches/kuhn_munkres_vs_bertsekas.rs index 30df16ff..73f523c7 100644 --- a/benches/kuhn_munkres_vs_bertsekas.rs +++ b/benches/kuhn_munkres_vs_bertsekas.rs @@ -1,5 +1,6 @@ -use codspeed_criterion_compat::Throughput; -use codspeed_criterion_compat::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use codspeed_criterion_compat::{ + black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, +}; use pathfinding::bertsekas::{forward, Auction}; use pathfinding::prelude::{kuhn_munkres, Matrix}; use rand::Rng; @@ -13,37 +14,36 @@ fn create_matrices(size: usize) -> (Matrix, Matrix) { fn compare_algorithms(c: &mut Criterion) { let mut group = c.benchmark_group("Assignment Problem"); - let sizes = [10, 20, 50, 100, 200, 500, 1000]; for size in sizes.iter() { - // Bertekas Auction - Time - group.bench_with_input( - BenchmarkId::new("Bertekas Auction Time", size), - size, - |b, &size| { - let (_, float_matrix) = create_matrices(size); - let mut auction_data = Auction::new(float_matrix); - b.iter(|| { - forward(&mut auction_data); - }); - }, - ); - - // Hungarian Algorithm - Time - group.bench_with_input( - BenchmarkId::new("Hungarian Algorithm Time", size), - size, - |b, &size| { - let (int_64matrix, _) = create_matrices(size); - b.iter(|| { - kuhn_munkres(&int_64matrix); - }); - }, - ); + let elements = size * size; + + group.throughput(Throughput::Elements(elements as u64)); + group.bench_function(BenchmarkId::new("Bertekas Auction", size), |b| { + let (_, float_matrix) = black_box(create_matrices(*size)); + b.iter_with_large_drop(|| { + let mut auction_data = Auction::new(&float_matrix); + forward(&mut auction_data); + }); + }); + + group.throughput(Throughput::Elements(elements as u64)); + group.bench_function(BenchmarkId::new("Hungarian Algorithm", size), |b| { + let (int_64matrix, _) = black_box(create_matrices(*size)); + b.iter_with_large_drop(|| kuhn_munkres(&int_64matrix)); + }); } + + // Configure the plot + group.plot_config( + codspeed_criterion_compat::PlotConfiguration::default() + .summary_scale(codspeed_criterion_compat::AxisScale::Logarithmic), + ); + group.finish(); } criterion_group!(benches, compare_algorithms); criterion_main!(benches); + diff --git a/src/bertsekas.rs b/src/bertsekas.rs index fc75b923..4d2bb4fa 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -1,13 +1,12 @@ //! Bertekas Auction Algorithm for the Assignment Problem -use num_traits::FloatConst; - -use crate::{matrix::Matrix, prelude::Weights}; +use crate::matrix::Matrix; +use std::marker::PhantomData; use std::sync::mpsc::{channel, Receiver, Sender}; /// A simple data structure that keeps track of all data required to /// assign agents to tasks. -pub struct Auction { - cost_matrix: Matrix, +pub struct Auction<'a, T> { + cost_matrix: &'a Matrix, assignments: Vec>, prices: Vec, epsilon: T, @@ -15,15 +14,20 @@ pub struct Auction { /// each agent made for that task. Thus, each task, j, has a list of (agent, bid) tuples /// that were put in by each agent, i. task_bids: Vec>, + _phantom: PhantomData, } -impl Auction +impl<'a, T> Auction<'a, T> where T: num_traits::Float, { /// Returns a new [`Auction`] based on the cost matrix used to determine optimal assignments. + /// + /// # Panics + /// + /// Panics if not able to covert `1 / (n + 1)` into the cost matrix's underlying type. #[must_use] - pub fn new(cost_matrix: Matrix) -> Self { + pub fn new(cost_matrix: &'a Matrix) -> Self { let m = cost_matrix.rows; let n = cost_matrix.columns; @@ -31,7 +35,9 @@ where let assignments = vec![None; m]; let task_bids = vec![Vec::with_capacity(m); n]; - let epsilon = T::from(n + 1).unwrap().recip(); + let epsilon = T::from(n + 1) + .expect("couldn't convert n + 1 = {n} + 1 to the required type!") + .recip(); Self { cost_matrix, @@ -39,10 +45,16 @@ where prices, epsilon, task_bids, + _phantom: PhantomData, } } /// Compute the score after assigning all agents to tasks + /// + /// # Panics + /// + /// Panics if getting an assignment `(i, j)`, where `i` is the agent assigned to task `j`, + /// is not found in the cost matrix. This shouldn't really happen. #[must_use] pub fn score(&self) -> Option where @@ -76,13 +88,13 @@ where /// The number of agents (i.e., the # of rows in the cost matrix.) #[must_use] pub fn num_agents(&self) -> usize { - self.cost_matrix.rows() + self.cost_matrix.rows } /// The number of tasks (i.e., the # of cols in the cost matrix.) #[must_use] pub fn num_tasks(&self) -> usize { - self.cost_matrix.columns() + self.cost_matrix.columns } fn num_assigned(&self) -> usize { @@ -204,6 +216,7 @@ where { let (tx, rx): (Sender>, Receiver>) = channel(); + // TODO: this needs to be cleaned up using the `Drop` functionality of channels. let mut num_bids = 0; for p in 0..auction_data.num_agents() { if auction_data.is_unassigned(p) { @@ -220,7 +233,7 @@ where } } - // println!("waiting to assign bids for tasks..."); + // TODO: this needs to be cleaned up using the `Drop` functionality of channels. for _ in 0..num_bids { let bid = rx.recv().unwrap(); @@ -294,7 +307,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{kuhn_munkres::kuhn_munkres, matrix}; + use crate::kuhn_munkres::kuhn_munkres; use rand::Rng; #[test] @@ -307,7 +320,7 @@ mod tests { ]; let matrix = Matrix::from_rows(matrix).unwrap(); - let mut auction_data = Auction::new(matrix); + let mut auction_data = Auction::new(&matrix); forward(&mut auction_data); let expected_assignments = vec![Some(2), Some(0), Some(3), Some(1)]; @@ -328,7 +341,7 @@ mod tests { ]; let matrix = Matrix::from_rows(matrix).unwrap(); - let mut auction_data = Auction::new(matrix); + let mut auction_data = Auction::new(&matrix); forward(&mut auction_data); // Any assignment is optimal since all profits are equal. @@ -348,7 +361,7 @@ mod tests { let matrix = vec![vec![42.0]]; let matrix = Matrix::from_rows(matrix).unwrap(); - let mut auction_data = Auction::new(matrix); + let mut auction_data = Auction::new(&matrix); forward(&mut auction_data); // The only assignment possible should be agent 0 @@ -360,7 +373,7 @@ mod tests { fn empty() { let matrix: Matrix = Matrix::new_empty(0); - let mut auction_data = Auction::new(matrix); + let mut auction_data = Auction::new(&matrix); forward(&mut auction_data); } @@ -369,7 +382,7 @@ mod tests { let m = 700; let matrix = Matrix::from_fn(m, m, |(i, j)| (i + j) as f64); - let mut auction_data = Auction::new(matrix); + let mut auction_data = Auction::new(&matrix); forward(&mut auction_data); assert!(auction_data.all_assigned()); @@ -389,7 +402,7 @@ mod tests { let float_matrix = int_matrix.clone().map(|value| value as f64); let now = std::time::Instant::now(); - let mut auction_data = Auction::new(float_matrix); + let mut auction_data = Auction::new(&float_matrix); forward(&mut auction_data); let elapsed = now.elapsed().as_micros(); println!("bertekas auction complete in {elapsed}"); From 2fb3c22aa6ff7e59917f08e8bcbc1a4c617b2d3e Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 16 Sep 2024 17:01:11 -0400 Subject: [PATCH 04/14] Add example for profiling --- examples/assignment.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 examples/assignment.rs diff --git a/examples/assignment.rs b/examples/assignment.rs new file mode 100644 index 00000000..01216b6c --- /dev/null +++ b/examples/assignment.rs @@ -0,0 +1,18 @@ +use pathfinding::bertsekas::{forward, Auction}; +use pathfinding::matrix::Matrix; +use rand::Rng; + +fn generate_random_matrix(rows: usize, cols: usize) -> Matrix { + let mut rng = rand::thread_rng(); + Matrix::from_fn(rows, cols, |_| rng.gen_range(0.0..100.0)) +} + +fn main() { + // let sizes = vec![100, 250, 500]; + + let size = 500; + let matrix = generate_random_matrix(size, size); + let mut auction_data = Auction::new(&matrix); + + forward(&mut auction_data); +} From 63bc9bb13b9060c4b7f27d1e680d8025f4721fce Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 16 Sep 2024 17:03:10 -0400 Subject: [PATCH 05/14] Add profile for profiling to manifest --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 4527def4..bb04a0cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,10 @@ regex = "1.10.6" trybuild = "1.0.99" version_check = "0.9.5" +[profile.profiling] +inherits = "release" +debug = true + [lints.clippy] module_name_repetitions = { level = "allow", priority = 1 } too_long_first_doc_paragraph = { level = "allow", priority = 1 } # Temporary From 57d80c84523db49ac7aed4892978442af8db64ef Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Mon, 16 Sep 2024 17:25:20 -0400 Subject: [PATCH 06/14] Cleanup assignment phase --- src/bertsekas.rs | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/src/bertsekas.rs b/src/bertsekas.rs index 4d2bb4fa..16f73128 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -139,9 +139,8 @@ where } } - /// Should this be public? #[must_use] - pub fn is_unassigned(&self, agent: usize) -> bool { + fn is_unassigned(&self, agent: usize) -> bool { self.assignments[agent].is_none() } @@ -237,22 +236,15 @@ where for _ in 0..num_bids { let bid = rx.recv().unwrap(); - // auction_data.add_task_bid(unassigned_agent, best_obj, bid_value); auction_data.add_task_bid(bid.agent, bid.task, bid.amount); } - // println!("bidding phase complete"); } fn assignment_phase(auction_data: &mut Auction) where T: num_traits::Float + num_traits::FloatConst, { - let (tx, rx): (Sender>>, Receiver>>) = channel(); - - let mut num_tasks = 0; - for _ in &auction_data.task_bids { - num_tasks += 1; - } + let (tx, rx): (Sender>, Receiver>) = channel(); for (task, bids) in auction_data.task_bids.iter().enumerate() { let mut max_bid = T::neg_infinity(); @@ -267,27 +259,24 @@ where } if let Some(bw) = bid_winner { - tx.send(Some(Bid::new(bw, task, max_bid))).unwrap(); - } else { - tx.send(None).unwrap(); + tx.send(Bid::new(bw, task, max_bid)).unwrap(); } } - // println!("sent all bids via tx in assignment phase"); - for _i in 0..num_tasks { - if let Some(Bid { - agent: bid_winner, - task, - amount: max_bid, - }) = rx.recv().unwrap() - { - auction_data.update_price(task, max_bid); - auction_data.assign(bid_winner, task); - } + drop(tx); + + for Bid { + agent: bid_winner, + task, + amount: max_bid, + } in rx + { + auction_data.update_price(task, max_bid); + auction_data.assign(bid_winner, task); } - auction_data.clear_task_bids(); // Clear bids after each assignment phase - // println!("assignment phase complete"); + // Clear bids after each assignment phase + auction_data.clear_task_bids(); } /// Run the forward auction only. The way to consider this From 421d525847fdd0453531c036f2271e90b3c1bb4e Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 17 Sep 2024 01:38:08 -0400 Subject: [PATCH 07/14] Cleanup bidding phase --- src/bertsekas.rs | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/bertsekas.rs b/src/bertsekas.rs index 16f73128..583bed71 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -1,5 +1,6 @@ //! Bertekas Auction Algorithm for the Assignment Problem use crate::matrix::Matrix; +use num_traits::{Float, FloatConst}; use std::marker::PhantomData; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -19,7 +20,7 @@ pub struct Auction<'a, T> { impl<'a, T> Auction<'a, T> where - T: num_traits::Float, + T: Float, { /// Returns a new [`Auction`] based on the cost matrix used to determine optimal assignments. /// @@ -58,7 +59,7 @@ where #[must_use] pub fn score(&self) -> Option where - T: num_traits::Float + std::ops::AddAssign, + T: Float + std::ops::AddAssign, { let mut res: T = T::zero(); @@ -76,7 +77,7 @@ where pub(crate) fn scale_epsilon(&mut self) where - T: num_traits::Float + num_traits::FloatConst + std::ops::MulAssign, + T: Float + FloatConst + std::ops::MulAssign, { self.epsilon *= T::from(1.01).unwrap(); } @@ -146,7 +147,7 @@ where fn add_task_bid(&mut self, agent: usize, task: usize, bid: T) where - T: num_traits::Float, + T: Float, { self.task_bids[task].push((agent, bid)); } @@ -169,7 +170,7 @@ struct Bid { impl Bid { pub fn new(agent: usize, task: usize, amount: T) -> Self where - T: num_traits::Float + num_traits::FloatConst, + T: Float + FloatConst, { Self { agent, @@ -181,7 +182,7 @@ impl Bid { fn bid(agent_row: &[T], prices: &[T], epsilon: T, unassigned_agent: usize, tx: &Sender>) where - T: num_traits::Float + num_traits::FloatConst, + T: Float + FloatConst, { let mut best_task = None; let mut best_profit = T::neg_infinity(); @@ -206,23 +207,14 @@ where } } -/// This is known as the "Jacobi bidding" version. -/// Essentially, all agents bid for tasks, and only then -/// do we make an assignment. +/// This is known as the "Jacobi bidding" version. Essentially, all agents +/// bid for tasks, and only then do we make an assignment. fn bid_phase(auction_data: &mut Auction) where - T: num_traits::Float + num_traits::FloatConst, + T: Float + FloatConst, { let (tx, rx): (Sender>, Receiver>) = channel(); - // TODO: this needs to be cleaned up using the `Drop` functionality of channels. - let mut num_bids = 0; - for p in 0..auction_data.num_agents() { - if auction_data.is_unassigned(p) { - num_bids += 1; - } - } - for p in 0..auction_data.num_agents() { if auction_data.is_unassigned(p) { let agent_row = &auction_data.cost_matrix.get_row(p).unwrap(); @@ -232,17 +224,16 @@ where } } - // TODO: this needs to be cleaned up using the `Drop` functionality of channels. - for _ in 0..num_bids { - let bid = rx.recv().unwrap(); + drop(tx); + for bid in rx { auction_data.add_task_bid(bid.agent, bid.task, bid.amount); } } fn assignment_phase(auction_data: &mut Auction) where - T: num_traits::Float + num_traits::FloatConst, + T: Float + FloatConst, { let (tx, rx): (Sender>, Receiver>) = channel(); @@ -284,7 +275,7 @@ where /// be assigned to a task after each bidding phase. pub fn forward(auction_data: &mut Auction) where - T: num_traits::Float + num_traits::FloatConst + std::ops::MulAssign, + T: Float + FloatConst + std::ops::MulAssign, { while !auction_data.all_assigned() { bid_phase(auction_data); From d6cc8059911c2dfca459ff3d9d5d1cff51eb6331 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 17 Sep 2024 02:29:45 -0400 Subject: [PATCH 08/14] More cleanup of assignment phase --- src/bertsekas.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/bertsekas.rs b/src/bertsekas.rs index 583bed71..5f0986d4 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -153,11 +153,11 @@ where } /// We need to clear out all the bids that each agent made for each task - fn clear_task_bids(&mut self) { - for bids in &mut self.task_bids { - bids.clear(); - } - } + // fn clear_task_bids(&mut self) { + // for bids in &mut self.task_bids { + // bids.clear(); + // } + // } } /// Tuple struct of (agent, task, bid) @@ -237,12 +237,11 @@ where { let (tx, rx): (Sender>, Receiver>) = channel(); - for (task, bids) in auction_data.task_bids.iter().enumerate() { + for (task, bids) in auction_data.task_bids.iter_mut().enumerate() { let mut max_bid = T::neg_infinity(); let mut bid_winner = None; - for b in bids { - let (agent, agents_bid) = *b; + for (agent, agents_bid) in bids.drain(..) { if agents_bid > max_bid { max_bid = agents_bid; bid_winner = Some(agent); @@ -267,7 +266,7 @@ where } // Clear bids after each assignment phase - auction_data.clear_task_bids(); + // auction_data.clear_task_bids(); } /// Run the forward auction only. The way to consider this From a0055cfd6e683baea13a9287d60fb020f6f34c1a Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 17 Sep 2024 02:30:36 -0400 Subject: [PATCH 09/14] Fix doc comment warning --- src/bertsekas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bertsekas.rs b/src/bertsekas.rs index 5f0986d4..a5d9851a 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -152,7 +152,7 @@ where self.task_bids[task].push((agent, bid)); } - /// We need to clear out all the bids that each agent made for each task + // /// We need to clear out all the bids that each agent made for each task // fn clear_task_bids(&mut self) { // for bids in &mut self.task_bids { // bids.clear(); From 32992ff3c8f5e7c2f6c49d317273e003201c46c8 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 17 Sep 2024 19:48:01 -0400 Subject: [PATCH 10/14] Remove cruising phase allocations of channels --- benches/kuhn_munkres_vs_bertsekas.rs | 5 +- examples/assignment.rs | 4 +- src/bertsekas.rs | 88 ++++++++++++++++++---------- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/benches/kuhn_munkres_vs_bertsekas.rs b/benches/kuhn_munkres_vs_bertsekas.rs index 73f523c7..8166ae3c 100644 --- a/benches/kuhn_munkres_vs_bertsekas.rs +++ b/benches/kuhn_munkres_vs_bertsekas.rs @@ -1,7 +1,7 @@ use codspeed_criterion_compat::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput, }; -use pathfinding::bertsekas::{forward, Auction}; +use pathfinding::bertsekas::{bertsekas_aaap, Auction}; use pathfinding::prelude::{kuhn_munkres, Matrix}; use rand::Rng; @@ -24,7 +24,7 @@ fn compare_algorithms(c: &mut Criterion) { let (_, float_matrix) = black_box(create_matrices(*size)); b.iter_with_large_drop(|| { let mut auction_data = Auction::new(&float_matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); }); }); @@ -46,4 +46,3 @@ fn compare_algorithms(c: &mut Criterion) { criterion_group!(benches, compare_algorithms); criterion_main!(benches); - diff --git a/examples/assignment.rs b/examples/assignment.rs index 01216b6c..2166f558 100644 --- a/examples/assignment.rs +++ b/examples/assignment.rs @@ -1,4 +1,4 @@ -use pathfinding::bertsekas::{forward, Auction}; +use pathfinding::bertsekas::{bertsekas_aaap, Auction}; use pathfinding::matrix::Matrix; use rand::Rng; @@ -14,5 +14,5 @@ fn main() { let matrix = generate_random_matrix(size, size); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); } diff --git a/src/bertsekas.rs b/src/bertsekas.rs index a5d9851a..3b805b37 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -11,11 +11,14 @@ pub struct Auction<'a, T> { assignments: Vec>, prices: Vec, epsilon: T, + epsilon_scaling_factor: T, /// This is a mapping of every single possible task and the corresponding set of bids that /// each agent made for that task. Thus, each task, j, has a list of (agent, bid) tuples /// that were put in by each agent, i. task_bids: Vec>, _phantom: PhantomData, + tx: Sender>>, + rx: Receiver>>, } impl<'a, T> Auction<'a, T> @@ -40,16 +43,37 @@ where .expect("couldn't convert n + 1 = {n} + 1 to the required type!") .recip(); + let (tx, rx): (Sender>>, Receiver>>) = channel(); + Self { cost_matrix, assignments, prices, epsilon, + epsilon_scaling_factor: T::from(2.0).unwrap(), task_bids, _phantom: PhantomData, + tx, + rx, } } + /// Same a [`Self::new`], except the user is given the option of setting a custom + /// `epsilon_scaling_factor` via the [`es`] parameter. + /// + /// # Notes + /// In general, the value of epsilon determines how quickly the algorithm + /// will converge. The higher the value, the more *aggressive* the bidding. + /// Epsilon scaling is required to keep this algorithm from exhibiting + /// psuedopolynomial run-time. The scaling factor determines how fast the value + /// of `epsilon` grows after each round of bidding/assigning. + #[must_use] + pub fn with_epsilon_scaling_factor(es: T, cost_matrix: &'a Matrix) -> Self { + let mut auc = Self::new(cost_matrix); + auc.epsilon_scaling_factor = es; + auc + } + /// Compute the score after assigning all agents to tasks /// /// # Panics @@ -79,7 +103,7 @@ where where T: Float + FloatConst + std::ops::MulAssign, { - self.epsilon *= T::from(1.01).unwrap(); + self.epsilon *= self.epsilon_scaling_factor; } pub(crate) fn update_price(&mut self, task: usize, price: T) { @@ -180,16 +204,21 @@ impl Bid { } } -fn bid(agent_row: &[T], prices: &[T], epsilon: T, unassigned_agent: usize, tx: &Sender>) -where +fn bid( + agent_row: &[T], + prices: &[T], + epsilon: T, + unassigned_agent: usize, + tx: &Sender>>, +) where T: Float + FloatConst, { let mut best_task = None; let mut best_profit = T::neg_infinity(); let mut next_best_profit = T::neg_infinity(); - for ((j, val), price_j) in agent_row.iter().enumerate().zip(prices.iter()) { - let profit = (*val) - (*price_j); + for ((j, &val), &price_j) in agent_row.iter().enumerate().zip(prices.iter()) { + let profit = val - price_j; if profit > best_profit { next_best_profit = best_profit; @@ -203,7 +232,7 @@ where if let Some(best_obj) = best_task { let bid_value = prices[best_obj] + best_profit - next_best_profit + epsilon; let bid_for_agent = Bid::new(unassigned_agent, best_obj, bid_value); - tx.send(bid_for_agent).unwrap(); + tx.send(Some(bid_for_agent)).unwrap(); } } @@ -213,20 +242,20 @@ fn bid_phase(auction_data: &mut Auction) where T: Float + FloatConst, { - let (tx, rx): (Sender>, Receiver>) = channel(); - for p in 0..auction_data.num_agents() { if auction_data.is_unassigned(p) { - let agent_row = &auction_data.cost_matrix.get_row(p).unwrap(); - let prices = &auction_data.prices; + let agent_row = auction_data.cost_matrix.get_row(p).unwrap(); + let prices = auction_data.prices.as_slice(); let eps = auction_data.epsilon; + let tx = auction_data.tx.clone(); bid(agent_row, prices, eps, p, &tx); } } - drop(tx); + // Send a sentinel value, `None`, indicating we are finished sending all bids. + auction_data.tx.send(None).unwrap(); - for bid in rx { + while let Some(bid) = auction_data.rx.recv().unwrap() { auction_data.add_task_bid(bid.agent, bid.task, bid.amount); } } @@ -235,8 +264,6 @@ fn assignment_phase(auction_data: &mut Auction) where T: Float + FloatConst, { - let (tx, rx): (Sender>, Receiver>) = channel(); - for (task, bids) in auction_data.task_bids.iter_mut().enumerate() { let mut max_bid = T::neg_infinity(); let mut bid_winner = None; @@ -249,30 +276,31 @@ where } if let Some(bw) = bid_winner { - tx.send(Bid::new(bw, task, max_bid)).unwrap(); + let tx = auction_data.tx.clone(); + tx.send(Some(Bid::new(bw, task, max_bid))).unwrap(); } } - drop(tx); + // Send a sentinel value, `None`, indicating we are finished finding the *new* best assigments. + auction_data.tx.send(None).unwrap(); - for Bid { + while let Some(Bid { agent: bid_winner, task, amount: max_bid, - } in rx + }) = auction_data.rx.recv().unwrap() { auction_data.update_price(task, max_bid); auction_data.assign(bid_winner, task); } - // Clear bids after each assignment phase - // auction_data.clear_task_bids(); + // Make sure to clear bids after each assignment phase + // Currently, this is not needed since we use [`drain`]. } -/// Run the forward auction only. The way to consider this -/// is that agents are going to bid for tasks. Agents will -/// be assigned to a task after each bidding phase. -pub fn forward(auction_data: &mut Auction) +/// Run the Bertsekas algorithm to create an assignment. The way to think about this is that agents +/// are going to bid for tasks. Agents will be assigned to a task after each bidding phase. +pub fn bertsekas_aaap(auction_data: &mut Auction) where T: Float + FloatConst + std::ops::MulAssign, { @@ -300,7 +328,7 @@ mod tests { let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); let expected_assignments = vec![Some(2), Some(0), Some(3), Some(1)]; assert_eq!(auction_data.assignments, expected_assignments); @@ -321,7 +349,7 @@ mod tests { let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); // Any assignment is optimal since all profits are equal. assert!(auction_data.all_assigned()); @@ -341,7 +369,7 @@ mod tests { let matrix = Matrix::from_rows(matrix).unwrap(); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); // The only assignment possible should be agent 0 let expected_assignments = vec![Some(0)]; @@ -353,7 +381,7 @@ mod tests { let matrix: Matrix = Matrix::new_empty(0); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); } #[test] @@ -362,7 +390,7 @@ mod tests { let matrix = Matrix::from_fn(m, m, |(i, j)| (i + j) as f64); let mut auction_data = Auction::new(&matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); assert!(auction_data.all_assigned()); assert_eq!(auction_data.num_assigned(), m); @@ -382,7 +410,7 @@ mod tests { let now = std::time::Instant::now(); let mut auction_data = Auction::new(&float_matrix); - forward(&mut auction_data); + bertsekas_aaap(&mut auction_data); let elapsed = now.elapsed().as_micros(); println!("bertekas auction complete in {elapsed}"); println!("score: {}", auction_data.score().unwrap()); From ea0ec6c5a6d3116cd9f9c9014629a650728e9b61 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Tue, 17 Sep 2024 20:16:34 -0400 Subject: [PATCH 11/14] Fix example so both matrices are generated at once --- examples/assignment.rs | 45 +++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/examples/assignment.rs b/examples/assignment.rs index 2166f558..ebc7a6bb 100644 --- a/examples/assignment.rs +++ b/examples/assignment.rs @@ -1,18 +1,49 @@ use pathfinding::bertsekas::{bertsekas_aaap, Auction}; +use pathfinding::kuhn_munkres::kuhn_munkres; use pathfinding::matrix::Matrix; use rand::Rng; +use std::time::Instant; -fn generate_random_matrix(rows: usize, cols: usize) -> Matrix { +fn generate_random_matrices(rows: usize, cols: usize) -> (Matrix, Matrix) { let mut rng = rand::thread_rng(); - Matrix::from_fn(rows, cols, |_| rng.gen_range(0.0..100.0)) + let random_numbers: Vec = (0..rows * cols).map(|_| rng.gen_range(0..100)).collect(); + + let matrix_int = Matrix::from_vec(rows, cols, random_numbers.clone()).unwrap(); + let matrix_float = Matrix::from_vec( + rows, + cols, + random_numbers.into_iter().map(|x| x as f64).collect(), + ) + .unwrap(); + + (matrix_float, matrix_int) } fn main() { - // let sizes = vec![100, 250, 500]; + let sizes = vec![5, 10, 50, 100, 250, 500, 1000]; + + for &size in &sizes { + println!("Matrix size: {size} x {size}"); + let (f_matrix, i_matrix) = generate_random_matrices(size, size); - let size = 500; - let matrix = generate_random_matrix(size, size); - let mut auction_data = Auction::new(&matrix); + // Bertsekas solver + let start = Instant::now(); + let mut auction_data = Auction::new(&f_matrix); + bertsekas_aaap(&mut auction_data); + let score = auction_data.score().unwrap(); + let duration = start.elapsed(); + println!( + "Bertsekas algo time elapsed: {:?} with score: {score}", + duration + ); - bertsekas_aaap(&mut auction_data); + // Kuhn-Munkres solver + let start = Instant::now(); + let (score, _) = kuhn_munkres(&i_matrix); + let duration = start.elapsed(); + println!( + "Hungarian algo time elapsed: {:?} with score: {score}", + duration + ); + } } From d2479d93797d4d23067856d033b181676cd986a5 Mon Sep 17 00:00:00 2001 From: Saveliy Yusufov Date: Wed, 18 Sep 2024 16:20:40 -0400 Subject: [PATCH 12/14] Fix example of assignment --- examples/assignment.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/assignment.rs b/examples/assignment.rs index ebc7a6bb..31a7ebdd 100644 --- a/examples/assignment.rs +++ b/examples/assignment.rs @@ -6,7 +6,7 @@ use std::time::Instant; fn generate_random_matrices(rows: usize, cols: usize) -> (Matrix, Matrix) { let mut rng = rand::thread_rng(); - let random_numbers: Vec = (0..rows * cols).map(|_| rng.gen_range(0..100)).collect(); + let random_numbers: Vec = (0..rows * cols).map(|_| rng.gen_range(1..500)).collect(); let matrix_int = Matrix::from_vec(rows, cols, random_numbers.clone()).unwrap(); let matrix_float = Matrix::from_vec( @@ -20,13 +20,12 @@ fn generate_random_matrices(rows: usize, cols: usize) -> (Matrix, Matrix Date: Thu, 19 Sep 2024 20:07:19 -0400 Subject: [PATCH 13/14] Create simple csv output for example assignment --- examples/assignment.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/assignment.rs b/examples/assignment.rs index 31a7ebdd..152031dc 100644 --- a/examples/assignment.rs +++ b/examples/assignment.rs @@ -20,28 +20,23 @@ fn generate_random_matrices(rows: usize, cols: usize) -> (Matrix, Matrix Date: Mon, 7 Oct 2024 12:52:41 -0400 Subject: [PATCH 14/14] Modify bids for tasks (bids by agents) into SoA --- src/bertsekas.rs | 78 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/src/bertsekas.rs b/src/bertsekas.rs index 3b805b37..9a2a715a 100644 --- a/src/bertsekas.rs +++ b/src/bertsekas.rs @@ -4,6 +4,40 @@ use num_traits::{Float, FloatConst}; use std::marker::PhantomData; use std::sync::mpsc::{channel, Receiver, Sender}; +/// This is a mapping of every single possible task and the corresponding set of bids that each +/// agent made for that task. Thus, each task, j, has a list of `(agent, bid)` tuples that were put +/// in by each agent, i. +struct BidsForTasks { + agents: Vec>, + bids: Vec>, +} + +impl BidsForTasks +where + T: Float, +{ + pub fn new(num_tasks: usize) -> Self { + Self { + agents: vec![Vec::new(); num_tasks], + bids: vec![Vec::new(); num_tasks], + } + } + + fn clear(&mut self) { + for agents in &mut self.agents { + agents.clear(); + } + for bids in &mut self.bids { + bids.clear(); + } + } + + fn add_bid(&mut self, task: usize, agent: usize, bid: T) { + self.agents[task].push(agent); + self.bids[task].push(bid); + } +} + /// A simple data structure that keeps track of all data required to /// assign agents to tasks. pub struct Auction<'a, T> { @@ -12,10 +46,7 @@ pub struct Auction<'a, T> { prices: Vec, epsilon: T, epsilon_scaling_factor: T, - /// This is a mapping of every single possible task and the corresponding set of bids that - /// each agent made for that task. Thus, each task, j, has a list of (agent, bid) tuples - /// that were put in by each agent, i. - task_bids: Vec>, + task_bids: BidsForTasks, _phantom: PhantomData, tx: Sender>>, rx: Receiver>>, @@ -32,12 +63,16 @@ where /// Panics if not able to covert `1 / (n + 1)` into the cost matrix's underlying type. #[must_use] pub fn new(cost_matrix: &'a Matrix) -> Self { + // The # of rows in the matrix corresponds to the # of agents let m = cost_matrix.rows; + + // The # of columns in the matrix corresponds to the # of tasks let n = cost_matrix.columns; + assert!(m <= n); + let prices = vec![T::zero(); n]; let assignments = vec![None; m]; - let task_bids = vec![Vec::with_capacity(m); n]; let epsilon = T::from(n + 1) .expect("couldn't convert n + 1 = {n} + 1 to the required type!") @@ -51,7 +86,7 @@ where prices, epsilon, epsilon_scaling_factor: T::from(2.0).unwrap(), - task_bids, + task_bids: BidsForTasks::new(n), _phantom: PhantomData, tx, rx, @@ -173,7 +208,7 @@ where where T: Float, { - self.task_bids[task].push((agent, bid)); + self.task_bids.add_bid(task, agent, bid); } // /// We need to clear out all the bids that each agent made for each task @@ -264,24 +299,32 @@ fn assignment_phase(auction_data: &mut Auction) where T: Float + FloatConst, { - for (task, bids) in auction_data.task_bids.iter_mut().enumerate() { + for (task, (agents, bids)) in auction_data + .task_bids + .agents + .iter() + .zip(&auction_data.task_bids.bids) + .enumerate() + { let mut max_bid = T::neg_infinity(); let mut bid_winner = None; - for (agent, agents_bid) in bids.drain(..) { - if agents_bid > max_bid { - max_bid = agents_bid; + for (&agent, &bid) in agents.iter().zip(bids.iter()) { + if bid > max_bid { + max_bid = bid; bid_winner = Some(agent); } } if let Some(bw) = bid_winner { - let tx = auction_data.tx.clone(); - tx.send(Some(Bid::new(bw, task, max_bid))).unwrap(); + auction_data + .tx + .send(Some(Bid::new(bw, task, max_bid))) + .unwrap(); } } - // Send a sentinel value, `None`, indicating we are finished finding the *new* best assigments. + // Send a sentinel value, `None`, indicating we are finished finding the *new* best assignments. auction_data.tx.send(None).unwrap(); while let Some(Bid { @@ -294,8 +337,8 @@ where auction_data.assign(bid_winner, task); } - // Make sure to clear bids after each assignment phase - // Currently, this is not needed since we use [`drain`]. + // Clear bids after each assignment phase + auction_data.task_bids.clear(); } /// Run the Bertsekas algorithm to create an assignment. The way to think about this is that agents @@ -361,6 +404,9 @@ mod tests { .count(), 4 ); + + let expected_score: f64 = 4000.0; + assert_eq!(auction_data.score().unwrap(), expected_score); } #[test]