#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::TracktorError;
#[derive(Debug, Clone, PartialEq)]
pub struct Assignment {
#[cfg(feature = "alloc")]
pub mapping: Vec<Option<usize>>,
pub cost: f64,
}
#[cfg(feature = "alloc")]
impl Assignment {
pub fn new(mapping: Vec<Option<usize>>, cost: f64) -> Self {
Self { mapping, cost }
}
pub fn num_assigned(&self) -> usize {
self.mapping.iter().filter(|x| x.is_some()).count()
}
pub fn pairs(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
self.mapping
.iter()
.enumerate()
.filter_map(|(row, col)| col.map(|c| (row, c)))
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
pub struct CostMatrix {
data: Vec<f64>,
rows: usize,
cols: usize,
}
#[cfg(feature = "alloc")]
impl CostMatrix {
pub fn from_vec(data: Vec<f64>, rows: usize, cols: usize) -> Result<Self, TracktorError> {
if data.len() != rows * cols {
return Err(TracktorError::AssignmentFailed);
}
Ok(Self { data, rows, cols })
}
pub fn filled(rows: usize, cols: usize, value: f64) -> Self {
Self {
data: vec![value; rows * cols],
rows,
cols,
}
}
pub fn zeros(rows: usize, cols: usize) -> Self {
Self::filled(rows, cols, 0.0)
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn get(&self, row: usize, col: usize) -> f64 {
self.data[row * self.cols + col]
}
pub fn set(&mut self, row: usize, col: usize, value: f64) {
self.data[row * self.cols + col] = value;
}
pub fn get_mut(&mut self, row: usize, col: usize) -> &mut f64 {
&mut self.data[row * self.cols + col]
}
}
#[cfg(feature = "alloc")]
pub fn hungarian(cost: &CostMatrix) -> Result<Assignment, TracktorError> {
let n_rows = cost.rows();
let n_cols = cost.cols();
if n_rows == 0 || n_cols == 0 {
return Ok(Assignment::new(vec![], 0.0));
}
let n = n_rows.max(n_cols);
let large = 1e20_f64;
let mut matrix = vec![large; n * n];
for i in 0..n_rows {
for j in 0..n_cols {
matrix[i * n + j] = cost.get(i, j);
}
}
let mut u = vec![0.0_f64; n]; let mut v = vec![0.0_f64; n];
let mut row_assignment: Vec<Option<usize>> = vec![None; n];
let mut col_assignment: Vec<Option<usize>> = vec![None; n];
for i in 0..n {
let mut min_to = vec![f64::INFINITY; n]; let mut way = vec![None::<usize>; n]; let mut used = vec![false; n];
let mut cur_row = i;
let mut cur_col: Option<usize> = None;
loop {
let mut min_val = f64::INFINITY;
let mut min_col = 0;
for j in 0..n {
if used[j] {
continue;
}
let reduced_cost = matrix[cur_row * n + j] - u[cur_row] - v[j];
if reduced_cost < min_to[j] {
min_to[j] = reduced_cost;
way[j] = cur_col;
}
if min_to[j] < min_val {
min_val = min_to[j];
min_col = j;
}
}
for j in 0..n {
if used[j] {
u[col_assignment[j].unwrap()] += min_val;
v[j] -= min_val;
} else {
min_to[j] -= min_val;
}
}
u[i] += min_val;
used[min_col] = true;
cur_col = Some(min_col);
if col_assignment[min_col].is_none() {
break;
}
cur_row = col_assignment[min_col].unwrap();
}
while let Some(col) = cur_col {
let prev_col = way[col];
if let Some(pc) = prev_col {
col_assignment[col] = col_assignment[pc];
} else {
col_assignment[col] = Some(i);
}
cur_col = prev_col;
}
}
for (j, col_asgn) in col_assignment.iter().enumerate().take(n) {
if let Some(i) = col_asgn {
row_assignment[*i] = Some(j);
}
}
let mut total_cost = 0.0;
let mut result_mapping = Vec::with_capacity(n_rows);
for (i, row_asgn) in row_assignment.iter().enumerate().take(n_rows) {
if let Some(j) = row_asgn {
if *j < n_cols {
total_cost += cost.get(i, *j);
result_mapping.push(Some(*j));
} else {
result_mapping.push(None);
}
} else {
result_mapping.push(None);
}
}
Ok(Assignment::new(result_mapping, total_cost))
}
#[cfg(feature = "alloc")]
pub fn hungarian_gated(
cost: &CostMatrix,
gate_threshold: f64,
) -> Result<Assignment, TracktorError> {
let mut gated = CostMatrix::zeros(cost.rows(), cost.cols());
let large = 1e20_f64;
for i in 0..cost.rows() {
for j in 0..cost.cols() {
let c = cost.get(i, j);
if c <= gate_threshold {
gated.set(i, j, c);
} else {
gated.set(i, j, large);
}
}
}
let mut result = hungarian(&gated)?;
for i in 0..result.mapping.len() {
if let Some(j) = result.mapping[i] {
if cost.get(i, j) > gate_threshold {
result.mapping[i] = None;
}
}
}
result.cost = result.pairs().map(|(i, j)| cost.get(i, j)).sum();
Ok(result)
}
#[cfg(feature = "alloc")]
pub fn auction(
cost: &CostMatrix,
epsilon: f64,
max_iter: usize,
) -> Result<Assignment, TracktorError> {
let n_rows = cost.rows();
let n_cols = cost.cols();
if n_rows == 0 || n_cols == 0 {
return Ok(Assignment::new(vec![], 0.0));
}
let mut prices = vec![0.0; n_cols];
let mut row_assignment: Vec<Option<usize>> = vec![None; n_rows];
let mut col_assignment: Vec<Option<usize>> = vec![None; n_cols];
for _ in 0..max_iter {
let mut any_unassigned = false;
for i in 0..n_rows {
if row_assignment[i].is_some() {
continue;
}
any_unassigned = true;
let mut best_j = 0;
let mut best_value = f64::NEG_INFINITY;
let mut second_best = f64::NEG_INFINITY;
for (j, price) in prices.iter().enumerate().take(n_cols) {
let value = -cost.get(i, j) - price; if value > best_value {
second_best = best_value;
best_value = value;
best_j = j;
} else if value > second_best {
second_best = value;
}
}
let bid_increment = best_value - second_best + epsilon;
prices[best_j] += bid_increment;
if let Some(prev_i) = col_assignment[best_j] {
row_assignment[prev_i] = None;
}
row_assignment[i] = Some(best_j);
col_assignment[best_j] = Some(i);
}
if !any_unassigned {
break;
}
}
let total_cost: f64 = row_assignment
.iter()
.enumerate()
.filter_map(|(i, j)| j.map(|j| cost.get(i, j)))
.sum();
Ok(Assignment::new(row_assignment, total_cost))
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
struct MurtyNode {
assignment: Assignment,
fixed: Vec<(usize, usize)>,
excluded: Vec<(usize, usize)>,
partition_row: usize,
}
#[cfg(feature = "alloc")]
impl MurtyNode {
fn new(assignment: Assignment) -> Self {
Self {
assignment,
fixed: Vec::new(),
excluded: Vec::new(),
partition_row: 0,
}
}
fn with_constraints(
assignment: Assignment,
fixed: Vec<(usize, usize)>,
excluded: Vec<(usize, usize)>,
partition_row: usize,
) -> Self {
Self {
assignment,
fixed,
excluded,
partition_row,
}
}
}
#[cfg(feature = "alloc")]
fn hungarian_constrained(
cost: &CostMatrix,
fixed: &[(usize, usize)],
excluded: &[(usize, usize)],
) -> Option<Assignment> {
let n_rows = cost.rows();
let n_cols = cost.cols();
if n_rows == 0 || n_cols == 0 {
return Some(Assignment::new(vec![], 0.0));
}
for &(r1, c1) in fixed {
for &(r2, c2) in fixed {
if r1 != r2 && c1 == c2 {
return None;
}
}
if excluded.contains(&(r1, c1)) {
return None;
}
}
let large = 1e20_f64;
let mut modified = CostMatrix::zeros(n_rows, n_cols);
for i in 0..n_rows {
for j in 0..n_cols {
modified.set(i, j, cost.get(i, j));
}
}
for &(row, col) in excluded {
if row < n_rows && col < n_cols {
modified.set(row, col, large);
}
}
for &(row, col) in fixed {
if row < n_rows && col < n_cols {
for j in 0..n_cols {
if j != col {
modified.set(row, j, large);
}
}
for i in 0..n_rows {
if i != row {
modified.set(i, col, large);
}
}
}
}
let result = hungarian(&modified).ok()?;
for &(row, col) in fixed {
if row < n_rows && result.mapping.get(row).copied().flatten() != Some(col) {
return None; }
}
for &(row, col) in excluded {
if row < n_rows && result.mapping.get(row).copied().flatten() == Some(col) {
return None; }
}
let real_cost: f64 = result
.mapping
.iter()
.enumerate()
.filter_map(|(i, &j)| j.filter(|&jj| jj < n_cols).map(|jj| cost.get(i, jj)))
.sum();
Some(Assignment::new(result.mapping, real_cost))
}
#[cfg(feature = "alloc")]
pub fn murty_k_best(cost: &CostMatrix, k: usize) -> Vec<Assignment> {
use alloc::collections::BinaryHeap;
use core::cmp::Ordering;
if k == 0 {
return Vec::new();
}
#[derive(Debug)]
struct HeapNode(MurtyNode);
impl PartialEq for HeapNode {
fn eq(&self, other: &Self) -> bool {
self.0.assignment.cost == other.0.assignment.cost
}
}
impl Eq for HeapNode {}
impl PartialOrd for HeapNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HeapNode {
fn cmp(&self, other: &Self) -> Ordering {
other
.0
.assignment
.cost
.partial_cmp(&self.0.assignment.cost)
.unwrap_or(Ordering::Equal)
}
}
let mut results: Vec<Assignment> = Vec::with_capacity(k);
let mut heap: BinaryHeap<HeapNode> = BinaryHeap::new();
let best = match hungarian(cost) {
Ok(a) => a,
Err(_) => return Vec::new(),
};
heap.push(HeapNode(MurtyNode::new(best)));
while results.len() < k {
let node = match heap.pop() {
Some(HeapNode(n)) => n,
None => break, };
results.push(node.assignment.clone());
if results.len() >= k {
break;
}
let n_rows = cost.rows();
let mapping = &node.assignment.mapping;
for row in node.partition_row..n_rows {
if let Some(col) = mapping[row] {
let mut child_fixed = node.fixed.clone();
let mut child_excluded = node.excluded.clone();
for (prev_row, prev_col) in mapping
.iter()
.enumerate()
.take(row)
.skip(node.partition_row)
.filter_map(|(r, c)| c.map(|col| (r, col)))
{
if !child_fixed.iter().any(|&(r, _)| r == prev_row) {
child_fixed.push((prev_row, prev_col));
}
}
child_excluded.push((row, col));
if let Some(child_assignment) =
hungarian_constrained(cost, &child_fixed, &child_excluded)
{
heap.push(HeapNode(MurtyNode::with_constraints(
child_assignment,
child_fixed,
child_excluded,
row,
)));
}
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_simple() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let result = hungarian(&cost).unwrap();
assert_eq!(result.num_assigned(), 3);
assert!(
(result.cost - 15.0).abs() < 0.01,
"Expected cost 15.0, got {}",
result.cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_asymmetric() {
let cost =
CostMatrix::from_vec(vec![10.0, 5.0, 13.0, 3.0, 15.0, 8.0, 7.0, 4.0, 12.0], 3, 3)
.unwrap();
let result = hungarian(&cost).unwrap();
assert_eq!(result.num_assigned(), 3);
assert!(
(result.cost - 20.0).abs() < 0.01,
"Expected cost 20.0, got {}",
result.cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_rectangular() {
let cost = CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2).unwrap();
let result = hungarian(&cost).unwrap();
assert!(result.num_assigned() <= 2);
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_gated() {
let cost = CostMatrix::from_vec(vec![1.0, 100.0, 100.0, 2.0], 2, 2).unwrap();
let result = hungarian_gated(&cost, 10.0).unwrap();
assert_eq!(result.num_assigned(), 2);
assert!((result.cost - 3.0).abs() < 0.1);
}
#[cfg(feature = "alloc")]
#[test]
fn test_auction() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let result = auction(&cost, 0.1, 100).unwrap();
assert_eq!(result.num_assigned(), 3);
assert!(
result.cost >= 14.5 && result.cost <= 15.5,
"Auction cost {} not within bounds",
result.cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_k1() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let hungarian_result = hungarian(&cost).unwrap();
let murty_results = murty_k_best(&cost, 1);
assert_eq!(murty_results.len(), 1);
assert!(
(murty_results[0].cost - hungarian_result.cost).abs() < 0.01,
"Murty k=1 should match Hungarian: {} vs {}",
murty_results[0].cost,
hungarian_result.cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_k3_ordering() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let results = murty_k_best(&cost, 3);
assert!(results.len() >= 2, "Should find at least 2 solutions");
for i in 1..results.len() {
assert!(
results[i].cost >= results[i - 1].cost - 0.001,
"Results should be ordered: cost[{}]={} >= cost[{}]={}",
i,
results[i].cost,
i - 1,
results[i - 1].cost
);
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_unique_solutions() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let results = murty_k_best(&cost, 5);
for i in 0..results.len() {
for j in (i + 1)..results.len() {
assert_ne!(
results[i].mapping, results[j].mapping,
"Solutions {} and {} should be different",
i, j
);
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_2x2() {
let cost = CostMatrix::from_vec(vec![1.0, 10.0, 10.0, 2.0], 2, 2).unwrap();
let results = murty_k_best(&cost, 2);
assert_eq!(results.len(), 2);
assert!(
(results[0].cost - 3.0).abs() < 0.01,
"Best should be 3.0, got {}",
results[0].cost
);
assert!(
(results[1].cost - 20.0).abs() < 0.01,
"Second best should be 20.0, got {}",
results[1].cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_3x3_comprehensive() {
let cost =
CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], 3, 3).unwrap();
let results = murty_k_best(&cost, 6);
assert!(
(results[0].cost - 15.0).abs() < 0.01,
"Optimal should be 15.0, got {}",
results[0].cost
);
for (idx, result) in results.iter().enumerate() {
let mut cols_used: Vec<bool> = vec![false; 3];
for &col in &result.mapping {
if let Some(c) = col {
assert!(
!cols_used[c],
"Solution {} has duplicate column assignment",
idx
);
cols_used[c] = true;
}
}
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_rectangular() {
let cost = CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2).unwrap();
let results = murty_k_best(&cost, 3);
assert!(!results.is_empty(), "Should find at least one solution");
for result in &results {
let assigned = result.mapping.iter().filter(|x| x.is_some()).count();
assert!(
assigned <= 2,
"Should assign at most 2 rows, got {}",
assigned
);
}
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_empty() {
let cost = CostMatrix::zeros(0, 0);
let results = murty_k_best(&cost, 5);
assert_eq!(results.len(), 1);
assert_eq!(results[0].cost, 0.0);
}
#[cfg(feature = "alloc")]
#[test]
fn test_murty_k0() {
let cost = CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
let results = murty_k_best(&cost, 0);
assert!(results.is_empty());
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_constrained_fixed() {
let cost = CostMatrix::from_vec(vec![1.0, 10.0, 10.0, 2.0], 2, 2).unwrap();
let fixed = vec![(0, 1)];
let excluded = vec![];
let result = hungarian_constrained(&cost, &fixed, &excluded);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.mapping[0], Some(1), "Row 0 should be fixed to col 1");
assert!(
(result.cost - 20.0).abs() < 0.01,
"Cost should be 20.0, got {}",
result.cost
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_constrained_excluded() {
let cost = CostMatrix::from_vec(vec![1.0, 10.0, 10.0, 2.0], 2, 2).unwrap();
let fixed = vec![];
let excluded = vec![(0, 0)];
let result = hungarian_constrained(&cost, &fixed, &excluded);
assert!(result.is_some());
let result = result.unwrap();
assert_ne!(
result.mapping[0],
Some(0),
"Row 0 should not be assigned to col 0"
);
}
#[cfg(feature = "alloc")]
#[test]
fn test_hungarian_constrained_infeasible() {
let cost = CostMatrix::from_vec(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
let fixed = vec![(0, 0), (1, 0)];
let excluded = vec![];
let result = hungarian_constrained(&cost, &fixed, &excluded);
assert!(result.is_none(), "Should be infeasible");
}
}