Import and Setup¶
First, let's set up the optimization level for better performance:
The :opt 3 command sets the Rust compiler optimization level to the highest setting, which significantly improves runtime performance for our trajectory distance calculations. This is especially important for computationally intensive operations like DTW, Frechet, and batch computations.
:opt 3
Optimization: 3
Now let's add the required dependencies. In this Jupyter notebook, we're using the local version of the project for demonstration purposes.
In a normal Rust project, you would add traj-dist-rs to your Cargo.toml file:
[dependencies]
traj-dist-rs = { version = "1.0.0-rc.2", features = ["parallel"] }
The parallel feature enables Rayon parallel processing for batch computation (pdist and cdist), which can significantly speed up calculations (typically 1.5x-3x faster).
:dep traj-dist-rs = { path = "../../" , features = ["parallel"]}
:dep rand = { version = "0.8" }
use traj_dist_rs::distance::sspd::sspd;
use traj_dist_rs::distance::dtw::dtw;
use traj_dist_rs::distance::hausdorff::hausdorff;
use traj_dist_rs::distance::lcss::lcss;
use traj_dist_rs::distance::edr::edr;
use traj_dist_rs::distance::erp::erp_standard;
use traj_dist_rs::distance::erp::erp_compat_traj_dist;
use traj_dist_rs::distance::discret_frechet::discret_frechet;
use traj_dist_rs::distance::distance_type::DistanceType;
use traj_dist_rs::distance::base::TrajectoryCalculator;
use traj_dist_rs::distance::batch::{pdist, cdist, Metric, DistanceAlgorithm};
Basic Concepts¶
The library provides implementations of several trajectory distance algorithms:
- SSPD: Symmetric Segment-Path Distance
- DTW: Dynamic Time Warping
- Hausdorff: Hausdorff Distance
- LCSS: Longest Common Subsequence
- EDR: Edit Distance on Real sequence
- ERP: Edit distance with Real Penalty
- Discret Frechet: Discrete Fréchet Distance
Each algorithm supports both Euclidean (Cartesian) and Spherical (Haversine) distance calculations.
Basic Usage¶
Let's start with basic usage of the traj-dist-rs library:
// Define two trajectories as vectors of [x, y] coordinates
let traj1: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2: Vec<[f64; 2]> = vec![[0.1, 0.1], [1.1, 1.1], [2.1, 2.1]];
println!("Trajectory 1: {:?}", traj1);
println!("Trajectory 2: {:?}", traj2);
Trajectory 1: [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]
Trajectory 2: [[0.1, 0.1], [1.1, 1.1], [2.1, 2.1]]
Now we can calculate different trajectory distances:
// Calculate different trajectory distances
let sspd_dist = sspd(&traj1, &traj2, DistanceType::Euclidean);
println!("SSPD distance: {:.6}", sspd_dist);
// For DTW, we need a TrajectoryCalculator
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let dtw_dist = dtw(&calculator, false);
println!("DTW distance: {:.6}", dtw_dist);
}
let hausdorff_dist = hausdorff(&traj1, &traj2, DistanceType::Euclidean);
println!("Hausdorff distance: {:.6}", hausdorff_dist);
SSPD distance: 0.047140
DTW distance: 0.4242640687119288
Hausdorff distance: 0.141421
Using Spherical Distance¶
For geographic coordinates (latitude/longitude), use spherical distance:
// Geographic coordinates as [latitude, longitude]
// NYC trajectory
let geo_traj1: Vec<[f64; 2]> = vec![
[40.7128, -74.0060],
[40.7589, -73.9851],
[40.7831, -73.9712]
];
// Similar NYC trajectory
let geo_traj2: Vec<[f64; 2]> = vec![
[40.7228, -74.0160],
[40.7689, -73.9951],
[40.7931, -73.9812]
];
println!("Geographic Trajectory 1 (NYC): {:?}", geo_traj1);
println!("Geographic Trajectory 2 (Similar to NYC): {:?}", geo_traj2);
Geographic Trajectory 1 (NYC): [[40.7128, -74.006], [40.7589, -73.9851], [40.7831, -73.9712]]
Geographic Trajectory 2 (Similar to NYC): [[40.7228, -74.016], [40.7689, -73.9951], [40.7931, -73.9812]]
Calculate distances using spherical distance (Haversine formula):
// Calculate distances using spherical distance (Haversine formula)
let sspd_dist = sspd(&geo_traj1, &geo_traj2, DistanceType::Spherical);
println!("SSPD spherical distance: {:.2} meters", sspd_dist);
{
let calculator = TrajectoryCalculator::new(&geo_traj1, &geo_traj2, DistanceType::Spherical);
let dtw_dist = dtw(&calculator, false);
println!("DTW spherical distance: {:.2} meters", dtw_dist);
}
SSPD spherical distance: 1836.07 meters
DTW spherical distance: 3464.2392876320278 meters
()
Algorithms with Parameters¶
Some algorithms require additional parameters:
let traj1: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
let traj2: Vec<[f64; 2]> = vec![[0.5, 0.5], [1.5, 1.5], [2.5, 2.5], [3.5, 3.5]];
println!("Using trajectories with parameters:");
println!("Trajectory 1: {:?}", traj1);
println!("Trajectory 2: {:?}", traj2);
Using trajectories with parameters:
Trajectory 1: [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]]
Trajectory 2: [[0.5, 0.5], [1.5, 1.5], [2.5, 2.5], [3.5, 3.5]]
// LCSS with epsilon parameter - threshold for point matching
{
let calculator_lcss = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let lcss_dist = lcss(&calculator_lcss, 1.0, false);
println!("LCSS distance (eps=1.0): {:.6}", lcss_dist);
}
// EDR with epsilon parameter - threshold for point matching
{
let calculator_edr = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let edr_dist = edr(&calculator_edr, 1.0, false);
println!("EDR distance (eps=1.0): {:.6}", edr_dist);
}
// ERP with gap point - reference point for penalties
{
let calculator_erp = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let gap_point = [0.0, 0.0];
let erp_dist = erp_compat_traj_dist(&calculator_erp, &gap_point, false);
println!("ERP distance (g=[0,0]): {:.6}", erp_dist);
}
LCSS distance (eps=1.0): 0
EDR distance (eps=1.0): 0
ERP distance (g=[0,0]): 2.8284271247461903
()
Comparing All Available Algorithms¶
Let's compare all available algorithms on the same trajectories:
// Create sample trajectories
let traj1: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2: Vec<[f64; 2]> = vec![[0.0, 1.0], [1.0, 0.0], [2.0, 1.0]];
println!("Comparing All Algorithms:");
println!("Trajectory 1: {:?}", traj1);
println!("Trajectory 2: {:?}", traj2);
Comparing All Algorithms:
Trajectory 1: [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]
Trajectory 2: [[0.0, 1.0], [1.0, 0.0], [2.0, 1.0]]
// Calculate all available distances
// Standard algorithms without parameters
let sspd_dist = sspd(&traj1, &traj2, DistanceType::Euclidean);
println!("SSPD: {:.6}", sspd_dist);
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let dtw_dist = dtw(&calculator, false);
println!("DTW: {:.6}", dtw_dist);
}
let hausdorff_dist = hausdorff(&traj1, &traj2, DistanceType::Euclidean);
println!("Hausdorff: {:.6}", hausdorff_dist);
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let discret_frechet_dist = discret_frechet(&calculator, false);
println!("Discret Frechet: {:.6}", discret_frechet_dist);
}
// Algorithms with parameters
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let lcss_dist = lcss(&calculator, 2.0, false);
println!("LCSS: {:.6}", lcss_dist);
}
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let edr_dist = edr(&calculator, 2.0, false);
println!("EDR: {:.6}", edr_dist);
}
let gap_point = [0.0, 0.0];
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let erp_compat_dist = erp_compat_traj_dist(&calculator, &gap_point, false);
println!("ERP (compat): {:.6}", erp_compat_dist);
}
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let erp_standard_dist = erp_standard(&calculator, &gap_point, false);
println!("ERP (standard): {:.6}", erp_standard_dist);
}
SSPD: 0.755922
DTW: 3
Hausdorff: 1.000000
Discret Frechet: 1
LCSS: 0
EDR: 0
ERP (compat): 3
ERP (standard): 3
()
Using DpResult (Distance with Optional Matrix)¶
Some algorithms return a DpResult object that contains both the distance value and optionally the alignment matrix:
let traj1: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2: Vec<[f64; 2]> = vec![[0.1, 0.1], [1.1, 1.1], [2.1, 2.1]];
// DTW with full matrix
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let result_with_matrix = dtw(&calculator, true);
println!("DTW distance: {}", result_with_matrix.distance);
println!("Has matrix: {}", result_with_matrix.matrix.is_some());
}
// DTW without matrix (faster)
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let result_no_matrix = dtw(&calculator, false);
println!("DTW distance (no matrix): {}", result_no_matrix.distance);
println!("Has matrix: {}", result_no_matrix.matrix.is_some());
}
DTW distance: 0.4242640687119288
Has matrix: true
DTW distance (no matrix): 0.4242640687119288
Has matrix: false
()
Batch Computation with pdist¶
Compute pairwise distances between multiple trajectories:
// List of trajectories
let trajectories: Vec<Vec<[f64; 2]>> = vec![
vec![[0.0, 0.0], [1.0, 1.0]],
vec![[0.1, 0.1], [1.1, 1.1]],
vec![[0.2, 0.2], [1.2, 1.2]],
vec![[0.3, 0.3], [1.3, 1.3]],
];
println!("Number of trajectories: {}", trajectories.len());
// Create metric configuration using Metric
let metric = Metric::new(
DistanceAlgorithm::SSPD,
DistanceType::Euclidean
);
// Compute pairwise distances (compressed format)
let distances = pdist(&trajectories, &metric, true).unwrap();
println!("Number of pairwise distances: {}", distances.len());
println!("Distances: {:?}", distances);
Number of trajectories: 4
Number of pairwise distances: 6
Distances: [0.07071067811865488, 0.14142135623730961, 0.21213203435596437, 0.0707106781186548, 0.1414213562373096, 0.07071067811865488]
Batch Computation with cdist¶
Compute cross-distance matrix between two trajectory collections:
// Two collections of trajectories
let trajectories_a: Vec<Vec<[f64; 2]>> = vec![
vec![[0.0, 0.0], [1.0, 1.0]],
vec![[0.1, 0.1], [1.1, 1.1]],
];
let trajectories_b: Vec<Vec<[f64; 2]>> = vec![
vec![[0.2, 0.2], [1.2, 1.2]],
vec![[0.3, 0.3], [1.3, 1.3]],
vec![[0.4, 0.4], [1.4, 1.4]],
];
println!("Collection A: {} trajectories", trajectories_a.len());
println!("Collection B: {} trajectories", trajectories_b.len());
// Create metric configuration
let metric = Metric::new(
DistanceAlgorithm::DTW,
DistanceType::Euclidean
);
// Compute cross-distance matrix
let distance_matrix = cdist(&trajectories_a, &trajectories_b, &metric, true).unwrap();
println!("Distance matrix shape: ({})", distance_matrix.len());
println!("Distance matrix:");
for dis in distance_matrix.iter() {
println!(" {:?}", dis);
}
Collection A: 2 trajectories
Collection B: 3 trajectories
Distance matrix shape: (6)
Distance matrix:
0.565685424949238
0.8485281374238571
1.131370849898476
0.28284271247461884
0.565685424949238
0.8485281374238569
()
Parallel Processing¶
Use parallel processing for faster batch computation with Rayon:
use std::time::Instant;
// Create a larger dataset
let trajectories: Vec<Vec<[f64; 2]>> = (0..10)
.map(|_| {
(0..1000)
.map(|_| [rand::random::<f64>() * 10.0, rand::random::<f64>() * 10.0])
.collect()
})
.collect();
println!("Number of trajectories: {}", trajectories.len());
// Create metric configuration
let metric = Metric::new(
DistanceAlgorithm::SSPD,
DistanceType::Euclidean
);
// Sequential computation
let start = Instant::now();
let distances_seq = pdist(&trajectories, &metric, false).unwrap();
let seq_time = start.elapsed();
// Parallel computation
let start = Instant::now();
let distances_par = pdist(&trajectories, &metric, true).unwrap();
let par_time = start.elapsed();
println!("Sequential time: {:.3}s", seq_time.as_secs_f64());
println!("Parallel time: {:.3}s", par_time.as_secs_f64());
println!("Speedup: {:.2}x", seq_time.as_secs_f64() / par_time.as_secs_f64());
println!("Results match: {}", distances_seq.len() == distances_par.len());
Number of trajectories: 10
Sequential time: 1.854s
Parallel time: 0.623s
Speedup: 2.97x
Results match: true
Using Metric API with Different Algorithms¶
The Metric class provides type-safe configuration for all algorithms:
let traj1: Vec<[f64; 2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2: Vec<[f64; 2]> = vec![[0.1, 0.1], [1.1, 1.1], [2.1, 2.1]];
// Create metrics for different algorithms
let metric_sspd = Metric::new(DistanceAlgorithm::SSPD, DistanceType::Euclidean);
let metric_dtw = Metric::new(DistanceAlgorithm::DTW, DistanceType::Euclidean);
let metric_hausdorff = Metric::new(DistanceAlgorithm::Hausdorff, DistanceType::Euclidean);
let metric_discret_frechet = Metric::new(DistanceAlgorithm::DiscretFrechet, DistanceType::Euclidean);
// Algorithms with parameters
let metric_lcss = Metric::new(DistanceAlgorithm::LCSS { eps: 0.5 }, DistanceType::Euclidean);
let metric_edr = Metric::new(DistanceAlgorithm::EDR { eps: 0.5 }, DistanceType::Euclidean);
let metric_erp = Metric::new(DistanceAlgorithm::ERP { g: [0.0, 0.0] }, DistanceType::Euclidean);
// Use metrics with batch computation
let trajectories = vec![traj1, traj2];
let distances_sspd = pdist(&trajectories, &metric_sspd, false).unwrap();
let distances_dtw = pdist(&trajectories, &metric_dtw, false).unwrap();
let distances_lcss = pdist(&trajectories, &metric_lcss, false).unwrap();
println!("SSPD distance: {:.4}", distances_sspd[0]);
println!("DTW distance: {:.4}", distances_dtw[0]);
println!("LCSS distance: {:.4}", distances_lcss[0]);
SSPD distance: 0.0471
DTW distance: 0.4243
LCSS distance: 0.0000
Performance Comparison¶
Let's compare the performance of different algorithms:
use std::time::Instant;
// Create random trajectory
fn create_random_trajectory(length: usize) -> Vec<[f64; 2]> {
(0..length)
.map(|_| [rand::random::<f64>() * 10.0, rand::random::<f64>() * 10.0])
.collect()
}
// Create test trajectories
let traj_short = create_random_trajectory(10);
println!("Performance Comparison (short trajectory - 10 points):");
// SSPD
let start = Instant::now();
let result_sspd = sspd(&traj_short, &traj_short, DistanceType::Euclidean);
let sspd_time = start.elapsed();
println!("SSPD: {:.6}s, distance: {:.6}", sspd_time.as_secs_f64(), result_sspd);
// DTW
{
let calculator = TrajectoryCalculator::new(&traj_short, &traj_short, DistanceType::Euclidean);
let start = Instant::now();
let result_dtw = dtw(&calculator, false);
let dtw_time = start.elapsed();
println!("DTW: {:.6}s, distance: {:.6}", dtw_time.as_secs_f64(), result_dtw);
}
// Hausdorff
let start = Instant::now();
let result_hausdorff = hausdorff(&traj_short, &traj_short, DistanceType::Euclidean);
let hausdorff_time = start.elapsed();
println!("Hausdorff: {:.6}s, distance: {:.6}", hausdorff_time.as_secs_f64(), result_hausdorff);
// Discret Frechet
{
let calculator = TrajectoryCalculator::new(&traj_short, &traj_short, DistanceType::Euclidean);
let start = Instant::now();
let result_frechet = discret_frechet(&calculator, false);
let frechet_time = start.elapsed();
println!("Discret Frechet: {:.6}s, distance: {:.6}", frechet_time.as_secs_f64(), result_frechet);
}
Performance Comparison (short trajectory - 10 points):
SSPD: 0.000006s, distance: 0.000000
DTW: 0.000001s, distance: 0
Hausdorff: 0.000004s, distance: 0.000000
Discret Frechet: 0.000002s, distance: 0
()
Real-World Data Usage¶
Here's an example of how to use the library with real-world trajectory data:
// Example with GPS-like data
fn create_gps_trajectory(start_lat: f64, start_lon: f64, num_points: usize) -> Vec<[f64; 2]> {
(0..num_points)
.map(|i| {
let lat = start_lat + i as f64 * 0.001 + (rand::random::<f64>() - 0.5) * 0.02;
let lon = start_lon + i as f64 * 0.001 + (rand::random::<f64>() - 0.5) * 0.02;
[lat, lon]
})
.collect()
}
// Create two similar GPS trajectories
let gps_traj1 = create_gps_trajectory(40.7128, -74.0060, 20); // NYC-like
let gps_traj2 = create_gps_trajectory(40.7130, -74.0062, 20); // Similar, slightly different start
println!("GPS Trajectory 1 (first 3 points): {:?}", &gps_traj1[..3]);
println!("GPS Trajectory 2 (first 3 points): {:?}", &gps_traj2[..3]);
GPS Trajectory 1 (first 3 points): [[40.70742840896904, -73.99953647814593], [40.70397251104584, -73.99504109871333], [40.7244552176051, -74.00007432424702]]
GPS Trajectory 2 (first 3 points): [[40.715275098177976, -74.0135060767809], [40.70767464117644, -74.01109908545418], [40.72354877701038, -74.01102553585926]]
// Calculate distances using spherical distance for GPS coordinates
let sspd_dist = sspd(&gps_traj1, &gps_traj2, DistanceType::Spherical);
println!("SSPD spherical distance: {:.2} meters", sspd_dist);
{
let calculator = TrajectoryCalculator::new(&gps_traj1, &gps_traj2, DistanceType::Spherical);
let dtw_dist = dtw(&calculator, false);
println!("DTW spherical distance: {:.2} meters", dtw_dist);
}
let hausdorff_dist = hausdorff(&gps_traj1, &gps_traj2, DistanceType::Spherical);
println!("Hausdorff spherical distance: {:.2} meters", hausdorff_dist);
SSPD spherical distance: 348.86 meters
DTW spherical distance: 15679.899950629464 meters
Hausdorff spherical distance: 752.86 meters
Summary¶
This guide covered:
- Import and setup in Rust
- Different trajectory distance algorithms
- Euclidean vs. Spherical distance calculations
- Algorithms with parameters (LCSS, EDR, ERP)
- Comparing all available algorithms
- Using DpResult for algorithm results
- Batch computation with pdist and cdist
- Parallel processing with Rayon
- Performance considerations
- Real-world GPS data usage
The traj-dist-rs library provides high-performance trajectory distance calculations with both Python and Rust APIs, making it suitable for a wide range of trajectory analysis applications.
For more information on each algorithm, refer to the API documentation.