#![cfg(feature = "alloc")]
use alloc::{vec, vec::Vec};
use super::errors::CrouseError;
use crate::traits::{
AssignmentState,
algorithms::weighted_assignment::lapjv::common::{
augmentation_backtrack, find_minimum_distance,
},
};
pub(crate) fn crouse_inner(
data: &[f64],
nr: usize,
nc: usize,
max_cost: f64,
) -> Result<Vec<(usize, usize)>, CrouseError> {
debug_assert!(nr <= nc, "crouse_inner requires nr <= nc");
debug_assert_eq!(data.len(), nr * nc);
if nr == 0 {
return Ok(Vec::new());
}
for &cost in data {
if !cost.is_finite() {
return Err(CrouseError::NonFiniteValues);
}
if cost <= 0.0 {
if cost == 0.0 {
return Err(CrouseError::ZeroValues);
}
return Err(CrouseError::NegativeValues);
}
if cost >= max_cost {
return Err(CrouseError::ValueTooLarge);
}
}
let mut column_costs = vec![0.0f64; nc];
let mut assigned_rows: Vec<AssignmentState<usize>> = vec![AssignmentState::Unassigned; nc];
let mut assigned_columns: Vec<AssignmentState<usize>> = vec![AssignmentState::Unassigned; nr];
let mut to_scan: Vec<usize> = vec![0; nc];
let mut predecessors: Vec<usize> = vec![0; nc];
let mut distances: Vec<f64> = vec![max_cost; nc];
for row in 0..nr {
for col in 0..nc {
to_scan[col] = col;
predecessors[col] = row;
distances[col] = data[row * nc + col] - column_costs[col];
}
let mut lower_bound: usize = 0;
let mut upper_bound: usize = 0;
let mut n_ready: usize = 0;
let sink_col = 'outer: loop {
if lower_bound == upper_bound {
n_ready = lower_bound;
upper_bound = find_minimum_distance(lower_bound, &distances, &mut to_scan);
for &col in &to_scan[lower_bound..upper_bound] {
if assigned_rows[col].is_unassigned() {
break 'outer col;
}
}
}
if let Some(col) = scan(
&mut lower_bound,
&mut upper_bound,
&mut to_scan,
&mut distances,
&mut predecessors,
&assigned_rows,
&column_costs,
data,
nc,
) {
break 'outer col;
}
};
let min_dist = distances[sink_col];
for &col in &to_scan[0..n_ready] {
column_costs[col] += distances[col] - min_dist;
}
augmentation_backtrack(
sink_col,
&predecessors,
&mut assigned_rows,
&mut assigned_columns,
row,
);
}
let mut result = Vec::with_capacity(nr);
for (row, state) in assigned_columns.into_iter().enumerate() {
let AssignmentState::Assigned(col) = state else {
unreachable!("Every row should be assigned after augmentation");
};
result.push((row, col));
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
fn scan(
lower_bound_ref: &mut usize,
upper_bound_ref: &mut usize,
to_scan: &mut [usize],
distances: &mut [f64],
predecessors: &mut [usize],
assigned_rows: &[AssignmentState<usize>],
column_costs: &[f64],
data: &[f64],
nc: usize,
) -> Option<usize> {
let mut lower_bound = *lower_bound_ref;
let mut upper_bound = *upper_bound_ref;
while lower_bound != upper_bound {
let col = to_scan[lower_bound];
lower_bound += 1;
let AssignmentState::Assigned(row) = assigned_rows[col] else {
unreachable!("Frontier column must be assigned during scan");
};
let min_dist = distances[col];
let initial_reduced = data[row * nc + col] - column_costs[col] - min_dist;
let current_upper = upper_bound;
for k in current_upper..to_scan.len() {
let neighbour_col = to_scan[k];
let reduced =
data[row * nc + neighbour_col] - column_costs[neighbour_col] - initial_reduced;
if reduced < distances[neighbour_col] {
distances[neighbour_col] = reduced;
predecessors[neighbour_col] = row;
if reduced.total_cmp(&min_dist).is_eq() {
if assigned_rows[neighbour_col].is_unassigned() {
*lower_bound_ref = lower_bound;
*upper_bound_ref = upper_bound;
return Some(neighbour_col);
}
to_scan[k] = to_scan[upper_bound];
to_scan[upper_bound] = neighbour_col;
upper_bound += 1;
}
}
}
}
*lower_bound_ref = lower_bound;
*upper_bound_ref = upper_bound;
None
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
#[test]
fn test_square_identity() {
let data = vec![
1.0, 10.0, 10.0, 10.0, 1.0, 10.0, 10.0, 10.0, 1.0, ];
let mut result = crouse_inner(&data, 3, 3, 100.0).unwrap();
result.sort_unstable();
assert_eq!(result, vec![(0, 0), (1, 1), (2, 2)]);
}
#[test]
fn test_rectangular_2x3() {
let data = vec![
10.0, 1.0, 5.0, 1.0, 10.0, 5.0, ];
let mut result = crouse_inner(&data, 2, 3, 100.0).unwrap();
result.sort_unstable();
assert_eq!(result, vec![(0, 1), (1, 0)]);
}
#[test]
fn test_empty() {
let result = crouse_inner(&[], 0, 0, 100.0).unwrap();
assert_eq!(result, [] as [(usize, usize); 0]);
}
#[test]
fn test_single_row() {
let data = vec![5.0, 1.0, 3.0];
let result = crouse_inner(&data, 1, 3, 100.0).unwrap();
assert_eq!(result, vec![(0, 1)]);
}
#[test]
fn test_validation_errors() {
assert!(matches!(
crouse_inner(&[f64::NAN], 1, 1, 100.0),
Err(CrouseError::NonFiniteValues)
));
assert!(matches!(crouse_inner(&[0.0], 1, 1, 100.0), Err(CrouseError::ZeroValues)));
assert!(matches!(crouse_inner(&[-1.0], 1, 1, 100.0), Err(CrouseError::NegativeValues)));
assert!(matches!(crouse_inner(&[100.0], 1, 1, 100.0), Err(CrouseError::ValueTooLarge)));
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn test_sparse_block_degenerate() {
let n = 218;
let non_edge = 1.0 + f64::EPSILON;
let max_cost = non_edge + 1.0;
let intensities: Vec<f64> =
(0..n).map(|i| 100.0 + (i as f64 * 7.3 % 50.0) * 100.0).collect();
let max_int = intensities.iter().copied().fold(0.0_f64, f64::max);
let mut data = vec![non_edge; n * n];
for i in 0..n {
let cluster_start = (i / 5) * 5;
let cluster_end = ((i / 5) + 1) * 5;
for j in cluster_start..cluster_end.min(n) {
data[i * n + j] =
1.0 + f64::EPSILON - (intensities[i] / max_int) * (intensities[j] / max_int);
}
}
let result = crouse_inner(&data, n, n, max_cost).unwrap();
let non_diagonal: Vec<_> = result.iter().filter(|&&(r, c)| r != c).copied().collect();
assert!(non_diagonal.is_empty(), "found {} non-diagonal assignments", non_diagonal.len());
}
}