pelorus 0.1.0

A high-performance GPS map-matching library for geospatial road network processing
Documentation
use approx::assert_abs_diff_eq;
use geo::Point;
use pelorus::probability::{
    emission_probability, emission_probability_from_distance, transition_probability,
};

// ============================================================================
// Unit Tests (moved from source modules)
// ============================================================================

#[test]
fn test_emission_probability_basic_unit() {
    let sigma = 4.07;

    // Test at 1 standard deviation (distance = sigma)
    let prob_1_std = emission_probability_from_distance(4.07, sigma);
    assert_abs_diff_eq!(prob_1_std, 0.2420, epsilon = 0.0001);

    // Test at distance 0
    let prob_0 = emission_probability_from_distance(0.0, sigma);
    assert_abs_diff_eq!(prob_0, 0.3989, epsilon = 0.0001);
}

#[test]
fn test_emission_probability_with_points_unit() {
    let sigma = 4.07;
    let gps = Point::new(0.0, 0.0);
    let candidate = Point::new(4.07, 0.0); // Distance = 4.07

    let prob = emission_probability(&gps, &candidate, sigma);
    assert_abs_diff_eq!(prob, 0.2420, epsilon = 0.0001);
}

#[test]
fn test_emission_probability_edge_cases_unit() {
    // Test large distance returns 0
    let prob_large = emission_probability_from_distance(250.0, 4.07);
    assert_eq!(prob_large, 0.0);

    // Test with points
    let gps = Point::new(0.0, 0.0);
    let far_candidate = Point::new(250.0, 0.0);
    let prob_large_points = emission_probability(&gps, &far_candidate, 4.07);
    assert_eq!(prob_large_points, 0.0);
}

#[test]
fn test_transition_probability_basic_unit() {
    let beta = 10.0;

    // Test with 100m great circle, 105m route distance
    let prob = transition_probability(100.0, 105.0, beta);
    assert_abs_diff_eq!(prob, 0.0607, epsilon = 0.0001);
}

#[test]
fn test_transition_probability_edge_cases_unit() {
    let beta = 10.0;

    // Test large difference returns 0
    let prob_large = transition_probability(100.0, 2500.0, beta);
    assert_eq!(prob_large, 0.0);
}

// ============================================================================
// Integration Tests
// ============================================================================

#[test]
fn test_emission_probability_at_one_std_dev() {
    let sigma = 4.07;
    let distance = 4.07; // 1 standard deviation

    let probability = emission_probability_from_distance(distance, sigma);

    // Expected: 1/√(2π*σ²) * e^(-0.5) ≈ 0.2420
    assert_abs_diff_eq!(probability, 0.2420, epsilon = 1e-4);
}

#[test]
fn test_emission_probability_at_zero_distance() {
    let sigma = 4.07;
    let distance = 0.0;

    let probability = emission_probability_from_distance(distance, sigma);

    // Expected: 1/√(2π*σ²) ≈ 0.3989
    assert_abs_diff_eq!(probability, 0.3989, epsilon = 1e-4);
}

#[test]
fn test_emission_probability_with_points() {
    let sigma = 4.07;
    let gps = Point::new(0.0, 0.0);
    let candidate = Point::new(4.07, 0.0); // Distance = 4.07

    let probability = emission_probability(&gps, &candidate, sigma);

    // Expected: 1/√(2π*σ²) * e^(-0.5) ≈ 0.2420
    assert_abs_diff_eq!(probability, 0.2420, epsilon = 1e-4);
}

#[test]
fn test_emission_probability_decreases_with_distance() {
    let sigma = 4.07;

    let prob_0 = emission_probability_from_distance(0.0, sigma);
    let prob_1 = emission_probability_from_distance(1.0, sigma);
    let prob_2 = emission_probability_from_distance(2.0, sigma);
    let prob_4 = emission_probability_from_distance(4.07, sigma);

    // Probability should decrease as distance increases
    assert!(prob_0 > prob_1);
    assert!(prob_1 > prob_2);
    assert!(prob_2 > prob_4);
}

#[test]
fn test_emission_probability_large_distance_cutoff() {
    let sigma = 4.07;

    // Test distances over 200m return 0
    assert_eq!(emission_probability_from_distance(200.1, sigma), 0.0);
    assert_eq!(emission_probability_from_distance(250.0, sigma), 0.0);
    assert_eq!(emission_probability_from_distance(1000.0, sigma), 0.0);

    // Test with points
    let gps = Point::new(0.0, 0.0);
    let far_candidate = Point::new(250.0, 0.0);
    assert_eq!(emission_probability(&gps, &far_candidate, sigma), 0.0);
}

#[test]
fn test_emission_probability_different_sigma_values() {
    let distance = 5.0;

    let prob_small_sigma = emission_probability_from_distance(distance, 2.0);
    let prob_large_sigma = emission_probability_from_distance(distance, 10.0);

    // Larger sigma should give higher probability for same distance
    assert!(prob_large_sigma > prob_small_sigma);
}

// ============================================================================
// Transition Probability Tests
// ============================================================================

#[test]
fn test_transition_probability_basic_case() {
    let great_circle_distance = 100.0;
    let route_distance = 105.0;
    let beta = 10.0;

    let probability =
        transition_probability(great_circle_distance, route_distance, beta);

    // Expected: (1/10) * e^(-5/10) ≈ 0.0607
    assert_abs_diff_eq!(probability, 0.0607, epsilon = 1e-4);
}

#[test]
fn test_transition_probability_perfect_match() {
    let great_circle_distance = 100.0;
    let route_distance = 100.0; // Exact match
    let beta = 10.0;

    let probability =
        transition_probability(great_circle_distance, route_distance, beta);

    // Expected: (1/10) * e^0 = 0.1
    assert_abs_diff_eq!(probability, 0.1, epsilon = 1e-4);
}

#[test]
fn test_transition_probability_route_shorter_than_great_circle() {
    let great_circle_distance = 105.0;
    let route_distance = 100.0; // Route is shorter (should use absolute difference)
    let beta = 10.0;

    let probability =
        transition_probability(great_circle_distance, route_distance, beta);

    // Expected: (1/10) * e^(-5/10) ≈ 0.0607 (same as when route is longer)
    assert_abs_diff_eq!(probability, 0.0607, epsilon = 1e-4);
}

#[test]
fn test_transition_probability_decreases_with_difference() {
    let great_circle_distance = 100.0;
    let beta = 10.0;

    let prob_0_diff = transition_probability(great_circle_distance, 100.0, beta);
    let prob_5_diff = transition_probability(great_circle_distance, 105.0, beta);
    let prob_10_diff = transition_probability(great_circle_distance, 110.0, beta);
    let prob_20_diff = transition_probability(great_circle_distance, 120.0, beta);

    // Probability should decrease as difference increases
    assert!(prob_0_diff > prob_5_diff);
    assert!(prob_5_diff > prob_10_diff);
    assert!(prob_10_diff > prob_20_diff);
}

#[test]
fn test_transition_probability_large_route_distance_cutoff() {
    let great_circle_distance = 100.0;
    let beta = 10.0;

    // Test route distances > great_circle + 2000m return 0
    assert_eq!(
        transition_probability(great_circle_distance, 2200.0, beta),
        0.0
    );
    assert_eq!(
        transition_probability(great_circle_distance, 5000.0, beta),
        0.0
    );
}

#[test]
fn test_transition_probability_different_beta_values() {
    let great_circle_distance = 100.0;
    let route_distance = 110.0;

    let prob_strict =
        transition_probability(great_circle_distance, route_distance, 5.0);
    let prob_lenient =
        transition_probability(great_circle_distance, route_distance, 20.0);

    // Larger beta should give higher probability for same difference
    assert!(prob_lenient > prob_strict);
}

// ============================================================================
// Edge Case Tests
// ============================================================================

#[test]
fn test_emission_probability_zero_sigma() {
    // Edge case: zero sigma causes infinite exponent (distance > 0)
    let prob = emission_probability_from_distance(1.0, 0.0);
    assert_eq!(prob, 0.0); // e^(-∞) = 0

    // At distance 0, should still give coefficient value
    let prob_zero_distance = emission_probability_from_distance(0.0, 0.0);
    assert_abs_diff_eq!(
        prob_zero_distance,
        1.0 / (2.0 * std::f64::consts::PI).sqrt(),
        epsilon = 1e-10
    );
}

#[test]
fn test_transition_probability_zero_beta() {
    // Edge case: zero beta should not cause division by zero
    let prob = transition_probability(100.0, 105.0, 0.0);
    assert!(prob.is_infinite() || prob.is_nan());
}

#[test]
fn test_probabilities_are_non_negative() {
    // All probabilities should be >= 0
    assert!(emission_probability_from_distance(10.0, 5.0) >= 0.0);
    assert!(emission_probability_from_distance(0.0, 5.0) >= 0.0);
    assert!(transition_probability(100.0, 110.0, 10.0) >= 0.0);
    assert!(transition_probability(110.0, 100.0, 10.0) >= 0.0);
}

#[test]
fn test_emission_probability_bounds() {
    let sigma = 4.07;

    // Emission probability should be <= 1
    assert!(emission_probability_from_distance(0.0, sigma) <= 1.0);
    assert!(emission_probability_from_distance(1.0, sigma) <= 1.0);
    assert!(emission_probability_from_distance(10.0, sigma) <= 1.0);
}

// ============================================================================
// Mathematical Property Tests
// ============================================================================

#[test]
fn test_emission_probability_symmetry() {
    let sigma = 5.0;
    let gps = Point::new(0.0, 0.0);
    let candidate1 = Point::new(3.0, 0.0);
    let candidate2 = Point::new(-3.0, 0.0);

    // Emission probability depends only on distance magnitude
    let prob1 = emission_probability(&gps, &candidate1, sigma);
    let prob2 = emission_probability(&gps, &candidate2, sigma);

    assert_abs_diff_eq!(prob1, prob2, epsilon = 1e-10);
}

#[test]
fn test_transition_probability_symmetry() {
    let beta = 10.0;

    // Transition probability should be symmetric w.r.t. which distance is larger
    let prob1 = transition_probability(100.0, 110.0, beta);
    let prob2 = transition_probability(110.0, 100.0, beta);

    assert_abs_diff_eq!(prob1, prob2, epsilon = 1e-10);
}

// ============================================================================
// Numerical Stability Tests
// ============================================================================

#[test]
fn test_emission_probability_small_sigma() {
    // Test with very small sigma values
    let small_sigma = 0.001;
    let prob = emission_probability_from_distance(0.0, small_sigma);
    assert!(prob.is_finite());
    assert!(prob > 0.0);
}

#[test]
fn test_transition_probability_large_difference() {
    // Test with large but valid differences
    let prob = transition_probability(100.0, 1900.0, 10.0); // 1800m difference, still < 2000m cutoff
    assert!(prob.is_finite());
    assert!(prob > 0.0);
    assert!(prob < 1e-10); // Should be very small but not zero
}