use super::*;
use crate::local_search::cpu;
fn instance(n: usize, scale: f32) -> SolomonInstance {
let rows: String = (0..=n)
.map(|node| {
let x = ((node * 17) % 71) as f32 * scale;
let y = ((node * 29) % 73) as f32 * scale;
format!("{node} {x} {y} 0 0 100 0\n")
})
.collect();
format!("Reduction\nVEHICLE\n1 100\nCUSTOMER\n{rows}")
.parse()
.unwrap()
}
#[test]
fn small_routes_return_none_without_cuda() {
let instance = instance(1, 1.0);
for nodes in [vec![], vec![1]] {
assert_eq!(
best_two_opt_move(&Route::from_nodes(nodes), &instance).unwrap(),
None
);
}
}
#[test]
fn rejects_bad_routes_and_instances_before_cuda() {
let instance = instance(3, 1.0);
for nodes in [vec![0], vec![usize::MAX], vec![4], vec![1, 1]] {
assert!(matches!(
best_two_opt_move(&Route::from_nodes(nodes), &instance),
Err(GpuEvaluationError::InvalidInput(_))
));
}
let route = Route::from_nodes(vec![1, 2]);
let mut bad = instance.clone();
bad.distance_matrix.pop();
assert!(matches!(
best_two_opt_move(&route, &bad),
Err(GpuEvaluationError::InvalidInput(_))
));
bad = instance.clone();
bad.distance_matrix[1] = f32::NAN;
assert!(matches!(
best_two_opt_move(&route, &bad),
Err(GpuEvaluationError::InvalidInput(_))
));
bad = instance.clone();
bad.num_nodes = usize::MAX;
assert!(matches!(
best_two_opt_move(&route, &bad),
Err(GpuEvaluationError::SizeOverflow)
));
let too_large = Route::from_nodes(vec![1; 65_536]);
assert!(matches!(
best_two_opt_move(&too_large, &instance),
Err(GpuEvaluationError::SizeOverflow)
));
}
fn matrix_oracle(values: &[f32], n: usize) -> Option<TwoOptMove> {
let mut candidates: Vec<_> = (0..n)
.flat_map(|i| {
(i + 1..n).map(move |j| TwoOptMove {
i,
j,
delta: values[i * n + j],
})
})
.filter(|item| item.delta.is_finite() && item.delta < 0.0)
.collect();
candidates.sort_by(|a, b| {
a.delta
.partial_cmp(&b.delta)
.unwrap()
.then(a.i.cmp(&b.i))
.then(a.j.cmp(&b.j))
});
candidates.first().copied()
}
fn check_matrix(stream: &Arc<CudaStream>, values: &[f32], n: usize) {
let device = stream.clone_htod(values).unwrap();
let actual = reduce_device_deltas(stream, device, n as u32).unwrap();
assert_eq!(actual, matrix_oracle(values, n), "matrix width {n}");
}
#[test]
#[ignore = "requires an NVIDIA GPU and CUDA driver"]
fn reduction_boundaries_ties_and_nonfinite_values() {
let context = CudaContext::new(0).unwrap();
let stream = context.default_stream();
for n in [1, 2, 15, 16, 17, 255, 256, 257] {
let mut values = vec![f32::INFINITY; n * n];
for i in 0..n {
for j in 0..n {
values[i * n + j] = if i >= j {
-f32::MAX
} else {
[f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.0, 0.0, 3.0][(i + j) % 6]
};
}
}
check_matrix(&stream, &values, n);
if n < 2 {
continue;
}
let last = (n - 2) * n + n - 1;
values[last] = -7.0;
check_matrix(&stream, &values, n);
values[1] = -7.0;
check_matrix(&stream, &values, n);
values[last] = f32::from_bits((-7.0f32).to_bits() + 1);
check_matrix(&stream, &values, n);
}
let n = 257;
let mut values = vec![f32::INFINITY; n * n];
values[255] = -1.0;
values[256] = -2.0;
check_matrix(&stream, &values, n);
values[255] = -2.0;
for _ in 0..5 {
check_matrix(&stream, &values, n);
}
}
#[test]
#[ignore = "requires an NVIDIA GPU and CUDA driver"]
fn best_move_matches_cpu_across_blocks_and_scales() {
for n in [2, 3, 15, 16, 17, 31, 32, 33, 65, 257] {
for scale in [0.001, 1.0, 10_000.0] {
let instance = instance(n, scale);
let mut nodes: Vec<_> = (1..=n).collect();
let mut seed = 0x12345678u32;
for i in (1..n).rev() {
seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
nodes.swap(i, seed as usize % (i + 1));
}
let route = Route::from_nodes(nodes);
let before = route.clone();
let expected = cpu::best_two_opt_move(&route, &instance);
let actual = best_two_opt_move(&route, &instance).unwrap();
assert_eq!(
actual.map(|m| (m.i, m.j)),
expected.map(|m| (m.i, m.j)),
"n={n}, scale={scale}"
);
if let (Some(actual), Some(expected)) = (actual, expected) {
assert!(
(actual.delta - expected.delta).abs() <= 1e-5 * expected.delta.abs().max(1.0)
);
let mut improved = route.clone();
cpu::apply_two_opt(&mut improved, actual.i, actual.j);
let cost = |route: &Route| {
let nodes: Vec<_> = std::iter::once(0)
.chain(route.nodes.iter().copied())
.chain(std::iter::once(0))
.collect();
nodes
.windows(2)
.map(|edge| f64::from(instance.distance(edge[0], edge[1])))
.sum::<f64>()
};
let difference = cost(&improved) - cost(&route);
assert!(difference < 0.0);
assert!(
(difference - f64::from(actual.delta)).abs() <= 1e-4 * cost(&route).max(1.0)
);
}
assert_eq!(route, before);
}
}
}
#[test]
#[ignore = "requires an NVIDIA GPU and CUDA driver"]
fn best_move_returns_none_for_optimal_and_equal_cost_routes() {
let mut instance = instance(4, 1.0);
let n = instance.num_nodes;
for i in 0..n {
for j in 0..n {
instance.distance_matrix[i * n + j] = i.abs_diff(j) as f32;
}
}
assert_eq!(
best_two_opt_move(&Route::from_nodes(vec![1, 2, 3, 4]), &instance).unwrap(),
None
);
for i in 0..n {
for j in 0..n {
instance.distance_matrix[i * n + j] = if i == j { 0.0 } else { 1.0 };
}
}
assert_eq!(
best_two_opt_move(&Route::from_nodes(vec![1, 2, 3, 4]), &instance).unwrap(),
None
);
instance.distance_matrix.fill(2.0);
for i in 0..n {
instance.distance_matrix[i * n + i] = 0.0;
}
for (i, j) in [(0, 2), (2, 0), (1, 3), (3, 1)] {
instance.distance_matrix[i * n + j] = 1.0;
}
let route = Route::from_nodes(vec![1, 2, 3]);
assert_eq!(
best_two_opt_move(&route, &instance).unwrap(),
Some(TwoOptMove {
i: 0,
j: 1,
delta: -2.0
})
);
}