Skip to main content

edit_distance/
edit_distance.rs

1//! Edit Distance Example
2//!
3//! This example demonstrates how to use weighted finite state transducers (WFSTs)
4//! to compute edit distance between strings. It shows:
5//! 1. Building an edit distance transducer with customizable weights for different operations
6//! 2. Computing edit distance with uniform weights (standard Levenshtein distance)
7//! 3. Computing edit distance with custom weights for insertions, deletions, and substitutions
8//! 4. Demonstrating how different weight schemes affect the computed distance
9//!
10//! Related examples:
11//! - string_alignment.rs: Extends this with path extraction and alignment visualization
12//! - spell_checking.rs: Uses edit distance for spell correction applications
13//!
14//! Usage:
15//! ```bash
16//! cargo run --example edit_distance
17//! ```
18
19use arcweight::prelude::*;
20
21/// Builds an FST that computes edit distance between strings using dynamic programming.
22/// This creates a simple, correct implementation that handles any characters.
23///
24/// # Arguments
25/// * `source` - The source string
26/// * `target` - The target string
27/// * `insertion_cost` - Cost of inserting a character
28/// * `deletion_cost` - Cost of deleting a character  
29/// * `substitution_cost` - Cost of substituting a character
30///
31/// # Returns
32/// An FST that computes the edit distance
33fn build_edit_distance_fst(
34    source: &str,
35    target: &str,
36    insertion_cost: f32,
37    deletion_cost: f32,
38    substitution_cost: f32,
39) -> VectorFst<TropicalWeight> {
40    let mut fst = VectorFst::new();
41    let source_chars: Vec<char> = source.chars().collect();
42    let target_chars: Vec<char> = target.chars().collect();
43    let m = source_chars.len();
44    let n = target_chars.len();
45
46    // Create states for the edit distance lattice
47    // State (i,j) represents having processed i chars from source and j chars from target
48    let mut states = vec![vec![]; m + 1];
49    for state_row in states.iter_mut().take(m + 1) {
50        for _j in 0..=n {
51            state_row.push(fst.add_state());
52        }
53    }
54
55    // Start state
56    fst.set_start(states[0][0]);
57
58    // Final state - end of both strings
59    fst.set_final(states[m][n], TropicalWeight::one());
60
61    // Add transitions
62    #[allow(clippy::needless_range_loop)] // Using i,j to index into DP table
63    for i in 0..=m {
64        for j in 0..=n {
65            let current = states[i][j];
66
67            // Match or substitution - advance both strings
68            if i < m && j < n {
69                let cost = if source_chars[i] == target_chars[j] {
70                    0.0 // match
71                } else {
72                    substitution_cost // substitution
73                };
74                fst.add_arc(
75                    current,
76                    Arc::new(
77                        source_chars[i] as u32,
78                        target_chars[j] as u32,
79                        TropicalWeight::new(cost),
80                        states[i + 1][j + 1],
81                    ),
82                );
83            }
84
85            // Deletion - advance source only
86            if i < m {
87                fst.add_arc(
88                    current,
89                    Arc::new(
90                        source_chars[i] as u32,
91                        0, // epsilon output
92                        TropicalWeight::new(deletion_cost),
93                        states[i + 1][j],
94                    ),
95                );
96            }
97
98            // Insertion - advance target only
99            if j < n {
100                fst.add_arc(
101                    current,
102                    Arc::new(
103                        0, // epsilon input
104                        target_chars[j] as u32,
105                        TropicalWeight::new(insertion_cost),
106                        states[i][j + 1],
107                    ),
108                );
109            }
110        }
111    }
112
113    fst
114}
115
116/// Compute edit distance between two strings with custom weights
117fn compute_edit_distance(
118    source: &str,
119    target: &str,
120    insertion_cost: f32,
121    deletion_cost: f32,
122    substitution_cost: f32,
123) -> Result<f32> {
124    // Build edit distance FST that directly computes the distance
125    let edit_fst = build_edit_distance_fst(
126        source,
127        target,
128        insertion_cost,
129        deletion_cost,
130        substitution_cost,
131    );
132
133    // Find shortest path from start to final state
134    let config = ShortestPathConfig::default();
135    let shortest: VectorFst<TropicalWeight> = shortest_path(&edit_fst, config)?;
136
137    // Get the cost from the shortest path
138    if let Some(start) = shortest.start() {
139        // Check if there's a path to a final state
140        let mut stack = vec![(start, 0.0)];
141        let mut visited = std::collections::HashSet::new();
142
143        while let Some((state, cost)) = stack.pop() {
144            if visited.contains(&state) {
145                continue;
146            }
147            visited.insert(state);
148
149            if shortest.is_final(state) {
150                return Ok(cost);
151            }
152
153            for arc in shortest.arcs(state) {
154                stack.push((arc.nextstate, cost + arc.weight.value()));
155            }
156        }
157    }
158
159    // If no path found, return infinity
160    Ok(f32::INFINITY)
161}
162
163fn main() -> Result<()> {
164    println!("Edit Distance Computation Example");
165    println!("=================================\n");
166
167    // Test pairs of strings
168    let test_pairs = vec![
169        ("kitten", "sitting"),
170        ("saturday", "sunday"),
171        ("hello", "hallo"),
172        ("abc", "abc"),
173        ("abc", "def"),
174        ("", "abc"),
175        ("abc", ""),
176    ];
177
178    // Example 1: Standard Levenshtein distance (all operations cost 1)
179    println!("1. Standard Levenshtein Distance (all operations cost 1.0):");
180    println!("-----------------------------------------------------------");
181    for (source, target) in &test_pairs {
182        let distance = compute_edit_distance(source, target, 1.0, 1.0, 1.0)?;
183        if distance.is_finite() {
184            println!("  '{source}' -> '{target}': {distance}");
185        } else {
186            println!("  '{source}' -> '{target}': No transformation found");
187        }
188    }
189
190    // Example 2: Custom weights - insertions are cheap, deletions are expensive
191    println!("\n2. Custom Weights (insert=0.5, delete=2.0, substitute=1.0):");
192    println!("------------------------------------------------------------");
193    for (source, target) in &test_pairs {
194        let distance = compute_edit_distance(source, target, 0.5, 2.0, 1.0)?;
195        if distance.is_finite() {
196            println!("  '{source}' -> '{target}': {distance}");
197        } else {
198            println!("  '{source}' -> '{target}': No transformation found");
199        }
200    }
201
202    // Example 3: Substitutions are very expensive (encourages insertions/deletions)
203    println!("\n3. Expensive Substitutions (insert=1.0, delete=1.0, substitute=3.0):");
204    println!("--------------------------------------------------------------------");
205    for (source, target) in &test_pairs {
206        let distance = compute_edit_distance(source, target, 1.0, 1.0, 3.0)?;
207        if distance.is_finite() {
208            println!("  '{source}' -> '{target}': {distance}");
209        } else {
210            println!("  '{source}' -> '{target}': No transformation found");
211        }
212    }
213
214    // Example 4: Detailed analysis of one transformation
215    println!("\n4. Detailed Analysis: 'kitten' -> 'sitting'");
216    println!("--------------------------------------------");
217    println!("This transformation requires:");
218    println!("- Substitute 'k' -> 's'");
219    println!("- Insert 'i' after 's'");
220    println!("- Keep 'itten' -> 'itting' (substitute 'e' -> 'i')");
221    println!("- Add 'g' at the end\n");
222
223    let configs = vec![
224        ("Uniform weights", 1.0, 1.0, 1.0),
225        ("Cheap insertions", 0.5, 1.0, 1.0),
226        ("Cheap deletions", 1.0, 0.5, 1.0),
227        ("Cheap substitutions", 1.0, 1.0, 0.5),
228    ];
229
230    for (name, ins, del, sub) in configs {
231        let distance = compute_edit_distance("kitten", "sitting", ins, del, sub)?;
232        if distance.is_finite() {
233            println!("  {name}: {distance} (ins={ins}, del={del}, sub={sub})");
234        } else {
235            println!("  {name}: No transformation found (ins={ins}, del={del}, sub={sub})");
236        }
237    }
238
239    // Example 5: Character-by-character transformations
240    println!("\n5. Step-by-step transformations:");
241    println!("---------------------------------");
242    let examples = vec![
243        ("cat", "cut", "Substitute 'a' -> 'u'"),
244        ("cat", "cats", "Insert 's' at end"),
245        ("cats", "cat", "Delete 's' from end"),
246        ("cat", "dog", "Replace all characters"),
247    ];
248
249    for (source, target, description) in examples {
250        let distance = compute_edit_distance(source, target, 1.0, 1.0, 1.0)?;
251        println!("  '{source}' -> '{target}': {description} (distance: {distance})");
252    }
253
254    Ok(())
255}