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.3", 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, PrecomputedDistanceCalculator};
use traj_dist_rs::distance::batch::{pdist, cdist, Metric, DistanceAlgorithm};
use traj_dist_rs::traits::{AsCoord, CoordSequence};
use traj_dist_rs::distance::spherical::{SphericalTrajectoryCache, great_circle_distance_cached};
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.2392876330396 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.856s
Parallel time: 0.623s
Speedup: 2.98x
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.000004s, distance: 0.000000
DTW: 0.000001s, distance: 0
Hausdorff: 0.000004s, distance: 0.000000
Discret Frechet: 0.000001s, 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.707903372134524, -73.99700275541164], [40.715445868350386, -74.00130821742236], [40.720171488156474, -74.00087400262875]]
GPS Trajectory 2 (first 3 points): [[40.703860804679536, -74.0030291211482], [40.72244657939741, -74.01262520856929], [40.7114131308621, -74.00635374996905]]
// 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: 301.25 meters
DTW spherical distance: 12703.201486290805 meters
Hausdorff spherical distance: 708.04 meters
Advanced Usage: Core Traits and Types¶
The library is built on a set of core traits that define how coordinates and trajectories are represented internally. In most cases the built-in Vec<[f64; 2]> type is all you need — the Basic Usage section above already demonstrates this. However, if your application works with custom data structures (e.g. structs with named fields, database records, or foreign types), these traits let you adapt your own types to work directly with every distance algorithm the library provides — without copying data into a new format.
The key insight is: any type that implements AsCoord and CoordSequence can be passed straight into sspd, dtw, hausdorff, and all other algorithms. This makes integration with your existing domain model straightforward.
AsCoord Trait¶
The AsCoord trait defines the interface for accessing x and y coordinates of a point in 2D space. It allows algorithms to uniformly access coordinate values regardless of the underlying representation.
Provided implementations:
[f64; 2]- Array of two f64 values&[f64; 2]- Reference to array
// AsCoord is already imported above.
let point = [3.0, 4.0];
println!("Point coordinates: x={}, y={}", point.x(), point.y());
fn print_coordinate<C: AsCoord>(coord: &C) {
println!("Coordinate: ({}, {})", coord.x(), coord.y());
}
print_coordinate(&point);
Point coordinates: x=3, y=4
Coordinate: (3, 4)
CoordSequence Trait¶
The CoordSequence trait defines the interface for accessing sequences of coordinates (trajectories). It provides methods to get the length and access individual coordinates.
Provided implementations:
Vec<[f64; 2]>- Vector of coordinate arrays
// CoordSequence and AsCoord are already imported above.
let trajectory = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
println!("Trajectory length: {}", trajectory.len());
println!("Is empty: {}", trajectory.is_empty());
let second_point = trajectory.get(1);
println!("Second point: ({}, {})", second_point.x(), second_point.y());
println!("\nAll points:");
for i in 0..trajectory.len() {
let p = trajectory.get(i);
println!(" Point {}: ({}, {})", i, p.x(), p.y());
}
Trajectory length: 4
Is empty: false
Second point: (1, 1)
All points:
Point 0: (0, 0)
Point 1: (1, 1)
Point 2: (2, 2)
Point 3: (3, 3)
()
DistanceCalculator Trait¶
The DistanceCalculator trait is the foundation for dynamic programming-based distance algorithms (DTW, LCSS, EDR, ERP, Discret Frechet). It abstracts the distance computation between trajectory elements.
Key methods:
dis_between(i, j): Distance between i-th point in first sequence and j-th point in second sequencecompute_dis_for_extra_point(seq_id, idx, anchor): Distance to an external anchor point (used by ERP)len_seq1(),len_seq2(): Lengths of the two sequences
// TrajectoryCalculator and DistanceType are already imported above.
// DistanceCalculator trait is used internally by algorithms (dtw, lcss, edr, erp, discret_frechet).
// You interact with it by passing a calculator to an algorithm function.
let traj1 = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2 = vec![[0.0, 1.0], [1.0, 0.0], [2.0, 1.0]];
// Pass a TrajectoryCalculator (which implements DistanceCalculator) to dtw
{
let calculator = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
let result = dtw(&calculator, false);
println!("DTW using TrajectoryCalculator: {:.4}", result.distance);
}
// The same algorithm also accepts PrecomputedDistanceCalculator
// (both implement DistanceCalculator - shown in the next cell)
DTW using TrajectoryCalculator: 3.0000
()
TrajectoryCalculator¶
TrajectoryCalculator is a concrete implementation of DistanceCalculator that computes distances on-the-fly from trajectory coordinate sequences. It's the most common way to use DP-based algorithms.
When to use:
- Computing distance between two trajectories directly
- When you have coordinate data and want to compute distances
- For algorithms like DTW, LCSS, EDR, ERP, Discret Frechet
// TrajectoryCalculator, DistanceType, dtw are already imported above.
let traj1 = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let traj2 = vec![[0.1, 0.1], [1.1, 1.1], [2.1, 2.1]];
{
let calc = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
println!("DTW (Euclidean): {:.4}", dtw(&calc, false).distance);
}
{
let calc = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Spherical);
println!("DTW (Spherical): {:.4}", dtw(&calc, false).distance);
}
DTW (Euclidean): 0.4243
DTW (Spherical): 47222.4954
()
PrecomputedDistanceCalculator¶
PrecomputedDistanceCalculator is another implementation of DistanceCalculator that uses a precomputed distance matrix. This is useful when:
Use cases:
- You have precomputed distances from external sources
- Working with NumPy arrays via Python bindings (zero-copy)
- Reusing distance matrices for multiple algorithm comparisons
Storage format: The distance matrix is stored as a flat 1D array in row-major order: index = i * seq2_len + j
// TrajectoryCalculator, PrecomputedDistanceCalculator, DistanceType, dtw
// are all already imported above.
// Precomputed distance matrix in row-major order (index = row * ncols + col)
let matrix = vec![
0.0f64, 1.0, 2.0, // point 0 → traj2[0], traj2[1], traj2[2]
1.0, 0.0, 1.0, // point 1 → traj2[0], traj2[1], traj2[2]
];
// PrecomputedDistanceCalculator implements DistanceCalculator,
// so it can be passed to any DP-based algorithm (dtw, lcss, edr, erp, discret_frechet)
{
let calc = PrecomputedDistanceCalculator::new(&matrix, 2, 3);
println!("DTW (precomputed): {:.4}", dtw(&calc, false).distance);
}
// Compare with TrajectoryCalculator on the same distance values
let traj1 = vec![[0.0, 0.0], [1.0, 0.0]];
let traj2 = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
{
let calc = TrajectoryCalculator::new(&traj1, &traj2, DistanceType::Euclidean);
println!("DTW (trajectory): {:.4}", dtw(&calc, false).distance);
}
DTW (precomputed): 1.0000
DTW (trajectory): 1.0000
()
Summary of Core Traits and Types¶
| Type | Kind | Module | Purpose |
|---|---|---|---|
AsCoord |
trait | traj_dist_rs::traits |
Access x,y coordinates |
CoordSequence |
trait | traj_dist_rs::traits |
Access trajectory data |
DistanceCalculator |
trait | traj_dist_rs::traits |
Interface for DP algorithms |
TrajectoryCalculator |
struct | traj_dist_rs::distance::base |
On-the-fly distance computation |
PrecomputedDistanceCalculator |
struct | traj_dist_rs::distance::base |
Precomputed matrix lookup |
| SphericalTrajectoryCache | struct | traj_dist_rs::distance::spherical | Precomputed per-trajectory spherical data |
Note: DistanceCalculator is also re-exported at traj_dist_rs::traits::DistanceCalculator for convenience.
TrajectoryCalculator and PrecomputedDistanceCalculator are struct implementations; they live in distance::base.
Performance Optimization: SphericalTrajectoryCache¶
When computing many trajectory distances using Haversine (spherical) distance, a significant portion of time is spent repeating the same trigonometric operations for each point pair. For a single trajectory with n points that participates in k distance comparisons, its radian conversions and cosine values are recomputed n × k times.
SphericalTrajectoryCache solves this by precomputing three vectors once per trajectory:
lng_rad— longitude in radianslat_rad— latitude in radianscos_lat— cosine of latitude in radians
This reduces the cost of each Haversine call from 6 trigonometric operations to 2 (only sin(Δlat/2) and sin(Δlon/2) remain), delivering:
- ~16% average improvement across all spherical algorithms vs. Cython
- Up to +31% for SSPD, +29% for EDR, +21% for Hausdorff
- +55% improvement for batch
cdistspherical on long trajectories (length=1000)
Automatic Optimization via TrajectoryCalculator¶
If you use TrajectoryCalculator (which drives DTW, LCSS, EDR, ERP, Discret Frechet), the cache is created automatically when you select DistanceType::Spherical. No changes to your code are needed.
// TrajectoryCalculator automatically builds SphericalTrajectoryCache
// when DistanceType::Spherical is selected — nothing extra needed.
let geo_traj1: Vec<[f64; 2]> = vec![
[-73.9857, 40.7484],
[-73.9712, 40.7614],
[-73.9865, 40.7580],
];
let geo_traj2: Vec<[f64; 2]> = vec![
[-73.9851, 40.7580],
[-73.9700, 40.7500],
[-73.9900, 40.7650],
];
// SphericalTrajectoryCache is created automatically inside TrajectoryCalculator:
{
let calc = TrajectoryCalculator::new(&geo_traj1, &geo_traj2, DistanceType::Spherical);
let result = dtw(&calc, false);
println!("DTW spherical (auto-cached): {:.2} m", result.distance);
}
DTW spherical (auto-cached): 3176.18 m
()
Manual Cache Usage¶
For algorithms that work directly with coordinates (SSPD, Hausdorff), or when you want to reuse the cache across multiple calls, you can build it yourself using SphericalTrajectoryCache::from_trajectory:
// SphericalTrajectoryCache and great_circle_distance_cached are already imported above.
let traj_a: Vec<[f64; 2]> = vec![
[-73.9857, 40.7484],
[-73.9712, 40.7614],
[-73.9865, 40.7580],
];
let traj_b: Vec<[f64; 2]> = vec![
[-73.9851, 40.7580],
[-73.9700, 40.7500],
[-73.9900, 40.7650],
];
// Build caches once per trajectory
let cache_a = SphericalTrajectoryCache::from_trajectory(&traj_a);
let cache_b = SphericalTrajectoryCache::from_trajectory(&traj_b);
// Compute all pairwise distances using cached values
let n_a = traj_a.len();
let n_b = traj_b.len();
for i in 0..n_a {
for j in 0..n_b {
let dist = great_circle_distance_cached(&cache_a, i, &cache_b, j);
println!("Distance [{i}][{j}]: {:.2} m", dist);
}
}
Distance [0][0]: 1069.86 m
Distance [0][1]: 1335.95 m
Distance [0][2]: 1883.14 m
Distance [1][0]: 1231.64 m
Distance [1][1]: 1273.07 m
Distance [1][2]: 1635.00 m
Distance [2][0]: 118.05 m
Distance [2][1]: 1651.99 m
Distance [2][2]: 833.25 m
()
When to Use SphericalTrajectoryCache¶
| Scenario | Recommendation |
|---|---|
| DP-based algorithms (DTW, LCSS, EDR, ERP, Discret Frechet) | Automatic — TrajectoryCalculator handles this |
| SSPD, Hausdorff | Automatic — built into algorithm internally |
| Custom code computing many point-pair distances for the same trajectory | Manual — build cache once, reuse for all pairs |
| One-off single distance call | Not needed — standard great_circle_distance() is fine |
Note: The cache is most effective when the same trajectory participates in many distance calls. For a single
sspd(t1, t2, ...)call, the overhead of building the cache is already amortized across all internal point comparisons.
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
- Core traits (
AsCoord,CoordSequence,DistanceCalculator) for adapting custom data types SphericalTrajectoryCachefor Haversine performance optimization
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.