vrp-gpu 0.2.0-alpha.1

Experimental CVRP library with CPU 2-opt and optional CUDA-selected local search
Documentation
use super::*;
use crate::instance::VehicleConfig;

fn search_instance() -> SolomonInstance {
    let points: [(f32, f32); 8] = [
        (0.0, 0.0),
        (0.0, 4.0),
        (3.0, 1.0),
        (6.0, 5.0),
        (5.0, 0.0),
        (2.0, 6.0),
        (9.0, 0.0),
        (9.0, 3.0),
    ];
    let distance_matrix = points
        .iter()
        .flat_map(|&(x, y)| {
            points
                .iter()
                .map(move |&(other_x, other_y)| (x - other_x).abs() + (y - other_y).abs())
        })
        .collect();
    SolomonInstance {
        name: "GpuSearch".into(),
        vehicle: VehicleConfig {
            num_vehicles: 2,
            capacity: 5.0,
        },
        num_nodes: points.len(),
        xs: points.iter().map(|&(x, _)| x).collect(),
        ys: points.iter().map(|&(_, y)| y).collect(),
        demands: [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0].to_vec(),
        ready_times: vec![0.0; points.len()],
        due_times: vec![100.0; points.len()],
        service_times: vec![0.0; points.len()],
        distance_matrix,
    }
}

fn cpu_selector(
    route: &Route,
    instance: &SolomonInstance,
) -> Result<Option<TwoOptMove>, GpuEvaluationError> {
    Ok(cpu::best_two_opt_move(route, instance))
}

#[test]
fn route_search_matches_cpu_for_multiple_moves() {
    let instance = search_instance();
    let mut route = Route::from_nodes(vec![1, 2, 3, 4, 5]);
    let before = route_cost_f64(&route, &instance);
    let mut expected = route.clone();
    cpu::two_opt_route(&mut expected, &instance);

    let report = search_route_with(&mut route, &instance, &mut cpu_selector).unwrap();

    assert_eq!(route, expected);
    assert_eq!(route.nodes, vec![1, 5, 3, 4, 2]);
    assert_eq!(report.accepted_moves, 2);
    assert_eq!(
        report.distance_improvement,
        before - route_cost_f64(&route, &instance)
    );
    assert_eq!(report.distance_improvement, 14.0);
    assert_eq!(
        search_route_with(&mut route, &instance, &mut cpu_selector).unwrap(),
        GpuSearchReport {
            accepted_moves: 0,
            distance_improvement: 0.0,
        }
    );
}

#[test]
fn small_routes_and_empty_solution_do_not_require_cuda() {
    let instance = search_instance();
    for nodes in [vec![], vec![1]] {
        let mut route = Route::from_nodes(nodes.clone());
        assert_eq!(
            two_opt_route(&mut route, &instance).unwrap(),
            GpuSearchReport {
                accepted_moves: 0,
                distance_improvement: 0.0,
            }
        );
        assert_eq!(route.nodes, nodes);
    }
    let mut solution = Solution::empty();
    assert_eq!(
        two_opt(&mut solution, &instance).unwrap(),
        GpuSearchReport {
            accepted_moves: 0,
            distance_improvement: 0.0,
        }
    );
}

#[test]
fn invalid_input_is_rejected_before_selector() {
    let instance = search_instance();
    for nodes in [vec![0], vec![8], vec![1, 1]] {
        let mut route = Route::from_nodes(nodes.clone());
        let mut called = false;
        let result = search_route_with(&mut route, &instance, &mut |_, _| {
            called = true;
            Ok(None)
        });
        assert!(matches!(result, Err(GpuEvaluationError::InvalidInput(_))));
        assert!(!called);
        assert_eq!(route.nodes, nodes);
    }

    let mut bad = instance.clone();
    bad.distance_matrix.pop();
    let mut route = Route::from_nodes(vec![1, 2]);
    assert!(matches!(
        search_route_with(&mut route, &bad, &mut cpu_selector),
        Err(GpuEvaluationError::InvalidInput(_))
    ));
    assert_eq!(route.nodes, vec![1, 2]);
}

#[test]
fn inconsistent_selection_is_rejected_without_mutation() {
    let instance = search_instance();
    let original = Route::from_nodes(vec![1, 2, 3, 4, 5]);
    for selected in [
        TwoOptMove {
            i: 1,
            j: 1,
            delta: -1.0,
        },
        TwoOptMove {
            i: 1,
            j: 5,
            delta: -1.0,
        },
        TwoOptMove {
            i: 0,
            j: 1,
            delta: f32::NAN,
        },
        TwoOptMove {
            i: 0,
            j: 1,
            delta: 0.0,
        },
    ] {
        let mut route = original.clone();
        assert!(matches!(
            search_route_with(&mut route, &instance, &mut |_, _| Ok(Some(selected))),
            Err(GpuEvaluationError::InconsistentMove)
        ));
        assert_eq!(route, original);
    }

    let mut route = Route::from_nodes(vec![1, 2]);
    let original = route.clone();
    assert!(matches!(
        search_route_with(&mut route, &instance, &mut |_, _| {
            Ok(Some(TwoOptMove {
                i: 0,
                j: 1,
                delta: -1.0,
            }))
        }),
        Err(GpuEvaluationError::InconsistentMove)
    ));
    assert_eq!(route, original);
}

#[test]
fn selector_failure_after_a_move_keeps_route_unchanged() {
    let instance = search_instance();
    let mut route = Route::from_nodes(vec![1, 2, 3, 4, 5]);
    let original = route.clone();
    let mut calls = 0;
    let result = search_route_with(&mut route, &instance, &mut |route, instance| {
        calls += 1;
        if calls == 1 {
            cpu_selector(route, instance)
        } else {
            Err(GpuEvaluationError::InvalidInput(
                "injected selector failure",
            ))
        }
    });
    assert!(matches!(result, Err(GpuEvaluationError::InvalidInput(_))));
    assert_eq!(calls, 2);
    assert_eq!(route, original);
}

#[test]
fn solution_search_matches_cpu_and_is_atomic() {
    let instance = search_instance();
    let original = Solution::new(vec![
        Route::from_nodes(vec![1, 2, 3, 4, 5]),
        Route::from_nodes(vec![6, 7]),
    ]);
    assert!(original.is_feasible(&instance));
    let mut expected = original.clone();
    cpu::two_opt(&mut expected, &instance);

    let mut solution = original.clone();
    let report = search_solution_with(&mut solution, &instance, &mut cpu_selector).unwrap();
    assert_eq!(solution, expected);
    assert!(solution.is_feasible(&instance));
    assert_eq!(report.accepted_moves, 2);
    assert_eq!(report.distance_improvement, 14.0);

    let mut solution = original.clone();
    let result = search_solution_with(&mut solution, &instance, &mut |route, instance| {
        if route.nodes.first() == Some(&6) {
            Err(GpuEvaluationError::InvalidInput(
                "injected second-route failure",
            ))
        } else {
            cpu_selector(route, instance)
        }
    });
    assert!(matches!(result, Err(GpuEvaluationError::InvalidInput(_))));
    assert_eq!(solution, original);

    let mut solution = original.clone();
    solution.routes[1] = Route::from_nodes(vec![0]);
    let invalid = solution.clone();
    let mut called = false;
    assert!(matches!(
        search_solution_with(&mut solution, &instance, &mut |_, _| {
            called = true;
            Ok(None)
        }),
        Err(GpuEvaluationError::InvalidInput(_))
    ));
    assert!(!called);
    assert_eq!(solution, invalid);
}

#[test]
#[ignore = "requires an NVIDIA GPU and CUDA driver"]
fn gpu_route_search_matches_cpu_and_recomputed_cost() {
    let instance = search_instance();
    let mut route = Route::from_nodes(vec![1, 2, 3, 4, 5]);
    let before = route_cost_f64(&route, &instance);
    let mut expected = route.clone();
    cpu::two_opt_route(&mut expected, &instance);

    let report = two_opt_route(&mut route, &instance).unwrap();

    assert_eq!(route, expected);
    assert_eq!(report.accepted_moves, 2);
    assert_eq!(
        report.distance_improvement,
        before - route_cost_f64(&route, &instance)
    );
    assert_eq!(report.distance_improvement, 14.0);
    assert_eq!(
        two_opt_route(&mut route, &instance).unwrap().accepted_moves,
        0
    );
}

#[test]
#[ignore = "requires an NVIDIA GPU and CUDA driver"]
fn gpu_solution_search_matches_cpu_and_preserves_feasibility() {
    let instance = search_instance();
    let mut solution = Solution::new(vec![
        Route::from_nodes(vec![1, 2, 3, 4, 5]),
        Route::from_nodes(vec![6, 7]),
    ]);
    let before: f64 = solution
        .routes
        .iter()
        .map(|route| route_cost_f64(route, &instance))
        .sum();
    let mut expected = solution.clone();
    cpu::two_opt(&mut expected, &instance);

    let report = two_opt(&mut solution, &instance).unwrap();

    let after: f64 = solution
        .routes
        .iter()
        .map(|route| route_cost_f64(route, &instance))
        .sum();
    assert_eq!(solution, expected);
    assert!(solution.is_feasible(&instance));
    assert_eq!(report.accepted_moves, 2);
    assert_eq!(report.distance_improvement, before - after);
    assert_eq!(report.distance_improvement, 14.0);
}