1use anyhow::Result;
21use arcweight::prelude::*;
22
23#[derive(Debug, Clone, PartialEq)]
25enum AlignmentOp {
26 Match(char, char), Substitute(char, char), Insert(char), Delete(char), }
31
32#[derive(Debug, Clone)]
34struct Alignment {
35 operations: Vec<AlignmentOp>,
36 cost: f32,
37 #[allow(dead_code)]
38 source: String,
39 #[allow(dead_code)]
40 target: String,
41}
42
43impl Alignment {
44 fn visualize(&self) -> String {
46 let mut source_line = String::new();
47 let mut alignment_line = String::new();
48 let mut target_line = String::new();
49
50 for op in &self.operations {
51 match op {
52 AlignmentOp::Match(s, t) => {
53 source_line.push(*s);
54 alignment_line.push('|');
55 target_line.push(*t);
56 }
57 AlignmentOp::Substitute(s, t) => {
58 source_line.push(*s);
59 alignment_line.push('*');
60 target_line.push(*t);
61 }
62 AlignmentOp::Insert(c) => {
63 source_line.push('-');
64 alignment_line.push('+');
65 target_line.push(*c);
66 }
67 AlignmentOp::Delete(c) => {
68 source_line.push(*c);
69 alignment_line.push('-');
70 target_line.push('-');
71 }
72 }
73 }
74
75 format!("{source_line}\n{alignment_line}\n{target_line}")
76 }
77
78 fn describe(&self) -> String {
80 let mut description = Vec::new();
81 let mut source_pos = 0;
82 let mut target_pos = 0;
83
84 for op in &self.operations {
85 match op {
86 AlignmentOp::Match(s, _) => {
87 description.push(format!(
88 "Match '{s}' at positions {source_pos}/{target_pos}"
89 ));
90 source_pos += 1;
91 target_pos += 1;
92 }
93 AlignmentOp::Substitute(s, t) => {
94 description.push(format!(
95 "Substitute '{s}' -> '{t}' at positions {source_pos}/{target_pos}"
96 ));
97 source_pos += 1;
98 target_pos += 1;
99 }
100 AlignmentOp::Insert(c) => {
101 description.push(format!("Insert '{c}' at target position {target_pos}"));
102 target_pos += 1;
103 }
104 AlignmentOp::Delete(c) => {
105 description.push(format!("Delete '{c}' at source position {source_pos}"));
106 source_pos += 1;
107 }
108 }
109 }
110
111 description.join("\n")
112 }
113}
114
115fn build_alignment_fst(
118 source: &str,
119 target: &str,
120 insertion_cost: f32,
121 deletion_cost: f32,
122 substitution_cost: f32,
123) -> VectorFst<TropicalWeight> {
124 let mut fst = VectorFst::new();
125 let source_chars: Vec<char> = source.chars().collect();
126 let target_chars: Vec<char> = target.chars().collect();
127 let m = source_chars.len();
128 let n = target_chars.len();
129
130 let mut states = vec![vec![]; m + 1];
132 for state_row in states.iter_mut().take(m + 1) {
133 for _j in 0..=n {
134 state_row.push(fst.add_state());
135 }
136 }
137
138 fst.set_start(states[0][0]);
140 fst.set_final(states[m][n], TropicalWeight::one());
141
142 const MATCH: u32 = 1;
145 const SUBSTITUTE: u32 = 2;
146 const INSERT: u32 = 3;
147 const DELETE: u32 = 4;
148
149 for i in 0..=m {
150 #[allow(clippy::needless_range_loop)]
151 for j in 0..=n {
152 let current_state = states[i][j];
153
154 if i < m {
156 let next_state = states[i + 1][j];
157 fst.add_arc(
158 current_state,
159 Arc::new(
160 source_chars[i] as u32,
161 DELETE,
162 TropicalWeight::new(deletion_cost),
163 next_state,
164 ),
165 );
166 }
167
168 if j < n {
170 let next_state = states[i][j + 1];
171 fst.add_arc(
172 current_state,
173 Arc::new(
174 0, INSERT,
176 TropicalWeight::new(insertion_cost),
177 next_state,
178 ),
179 );
180 }
181
182 if i < m && j < n {
184 let next_state = states[i + 1][j + 1];
185 let source_char = source_chars[i] as u32;
186 let target_char = target_chars[j] as u32;
187
188 if source_char == target_char {
189 fst.add_arc(
191 current_state,
192 Arc::new(source_char, MATCH, TropicalWeight::one(), next_state),
193 );
194 } else {
195 fst.add_arc(
197 current_state,
198 Arc::new(
199 source_char,
200 SUBSTITUTE,
201 TropicalWeight::new(substitution_cost),
202 next_state,
203 ),
204 );
205 }
206 }
207 }
208 }
209
210 fst
211}
212
213fn extract_alignment_from_path(
215 source: &str,
216 target: &str,
217 shortest_path_fst: &VectorFst<TropicalWeight>,
218) -> Result<Alignment> {
219 let source_chars: Vec<char> = source.chars().collect();
220 let target_chars: Vec<char> = target.chars().collect();
221 let mut operations = Vec::new();
222 let mut total_cost = 0.0;
223
224 let start_state = shortest_path_fst.start().unwrap();
226 let mut current_state = start_state;
227 let mut source_pos = 0;
228 let mut target_pos = 0;
229
230 let mut arc_iter = shortest_path_fst.arcs(current_state);
232 let mut has_arcs = arc_iter.by_ref().count() > 0;
233
234 while current_state != shortest_path_fst.start().unwrap() || has_arcs {
235 let mut found_arc = false;
236
237 if let Some(arc) = shortest_path_fst.arcs(current_state).next() {
238 let _input_symbol = arc.ilabel;
240 let output_symbol = arc.olabel;
241 let weight = arc.weight.value();
242
243 total_cost += weight;
244
245 match output_symbol {
246 1 => {
247 if source_pos < source_chars.len() && target_pos < target_chars.len() {
249 operations.push(AlignmentOp::Match(
250 source_chars[source_pos],
251 target_chars[target_pos],
252 ));
253 source_pos += 1;
254 target_pos += 1;
255 }
256 }
257 2 => {
258 if source_pos < source_chars.len() && target_pos < target_chars.len() {
260 operations.push(AlignmentOp::Substitute(
261 source_chars[source_pos],
262 target_chars[target_pos],
263 ));
264 source_pos += 1;
265 target_pos += 1;
266 }
267 }
268 3 => {
269 if target_pos < target_chars.len() {
271 operations.push(AlignmentOp::Insert(target_chars[target_pos]));
272 target_pos += 1;
273 }
274 }
275 4 => {
276 if source_pos < source_chars.len() {
278 operations.push(AlignmentOp::Delete(source_chars[source_pos]));
279 source_pos += 1;
280 }
281 }
282 _ => {
283 found_arc = false;
285 }
286 }
287
288 if output_symbol <= 4 {
289 current_state = arc.nextstate;
290 found_arc = true;
291 }
292 }
293
294 if !found_arc {
295 break;
296 }
297
298 let mut arc_iter = shortest_path_fst.arcs(current_state);
300 has_arcs = arc_iter.by_ref().count() > 0;
301 }
302
303 Ok(Alignment {
304 operations,
305 cost: total_cost,
306 source: source.to_string(),
307 target: target.to_string(),
308 })
309}
310
311fn compute_alignment_fst(source: &str, target: &str) -> Result<Alignment> {
313 let alignment_fst = build_alignment_fst(source, target, 1.0, 1.0, 1.0);
315
316 let shortest = shortest_path(&alignment_fst, ShortestPathConfig::default())?;
318
319 extract_alignment_from_path(source, target, &shortest)
321}
322
323fn demonstrate_string_alignment() -> Result<()> {
325 println!("String Alignment Example");
326 println!("========================\n");
327
328 println!("1. FST-based Alignment Computation:");
330 println!("------------------------------------");
331
332 let test_pairs = vec![("kitten", "sitting"), ("hello", "hallo"), ("cat", "dog")];
333
334 for (source, target) in &test_pairs {
335 println!("\nComputing alignment for '{source}' -> '{target}':");
336 match compute_alignment_fst(source, target) {
337 Ok(alignment) => {
338 let cost = alignment.cost;
339 println!("FST-computed cost: {cost}");
340 println!("Visualization:");
341 let viz = alignment.visualize();
342 println!("{viz}");
343 }
344 Err(e) => {
345 println!("Error computing FST alignment: {e}");
346 println!("Falling back to manual example...");
347 }
348 }
349 }
350
351 println!("\n2. Manual Alignment Examples (for comparison):");
352 println!("-----------------------------------------------");
353
354 let examples = vec![
356 (
357 "kitten",
358 "sitting",
359 vec![
360 AlignmentOp::Substitute('k', 's'),
361 AlignmentOp::Match('i', 'i'),
362 AlignmentOp::Match('t', 't'),
363 AlignmentOp::Match('t', 't'),
364 AlignmentOp::Substitute('e', 'i'),
365 AlignmentOp::Insert('n'),
366 AlignmentOp::Substitute('n', 'g'),
367 ],
368 3.0,
369 ),
370 (
371 "hello",
372 "hallo",
373 vec![
374 AlignmentOp::Match('h', 'h'),
375 AlignmentOp::Substitute('e', 'a'),
376 AlignmentOp::Match('l', 'l'),
377 AlignmentOp::Match('l', 'l'),
378 AlignmentOp::Match('o', 'o'),
379 ],
380 1.0,
381 ),
382 (
383 "cat",
384 "dog",
385 vec![
386 AlignmentOp::Substitute('c', 'd'),
387 AlignmentOp::Substitute('a', 'o'),
388 AlignmentOp::Substitute('t', 'g'),
389 ],
390 3.0,
391 ),
392 ];
393
394 for (source, target, operations, cost) in examples {
395 let alignment = Alignment {
396 operations,
397 cost,
398 source: source.to_string(),
399 target: target.to_string(),
400 };
401
402 println!("\nAligning '{source}' -> '{target}':");
403 let cost = alignment.cost;
404 println!("Cost: {cost}");
405 println!("Visualization:");
406 let viz = alignment.visualize();
407 println!("{viz}");
408 }
409
410 println!("\n3. Multiple Optimal Alignments:");
412 println!("--------------------------------");
413 println!("For transforming 'abc' -> 'aec' (cost 1):");
414
415 let alignment1 = Alignment {
416 operations: vec![
417 AlignmentOp::Match('a', 'a'),
418 AlignmentOp::Substitute('b', 'e'),
419 AlignmentOp::Match('c', 'c'),
420 ],
421 cost: 1.0,
422 source: "abc".to_string(),
423 target: "aec".to_string(),
424 };
425
426 println!("\nOption 1 - Direct substitution:");
427 let viz = alignment1.visualize();
428 println!("{viz}");
429
430 println!("\nOption 2 - Delete and insert (if costs were equal):");
432 let alignment2 = Alignment {
433 operations: vec![
434 AlignmentOp::Match('a', 'a'),
435 AlignmentOp::Delete('b'),
436 AlignmentOp::Insert('e'),
437 AlignmentOp::Match('c', 'c'),
438 ],
439 cost: 2.0,
440 source: "abc".to_string(),
441 target: "aec".to_string(),
442 };
443 let viz = alignment2.visualize();
444 println!("{viz}");
445
446 println!("\n4. Biological Sequence Alignment:");
448 println!("---------------------------------");
449
450 let dna_alignment = Alignment {
451 operations: vec![
452 AlignmentOp::Match('a', 'a'),
453 AlignmentOp::Match('c', 'c'),
454 AlignmentOp::Substitute('g', 't'),
455 AlignmentOp::Match('t', 't'),
456 AlignmentOp::Match('a', 'a'),
457 AlignmentOp::Match('c', 'c'),
458 AlignmentOp::Match('g', 'g'),
459 AlignmentOp::Match('t', 't'),
460 ],
461 cost: 1.0,
462 source: "acgtacgt".to_string(),
463 target: "acttacgt".to_string(),
464 };
465
466 println!("DNA sequence alignment:");
467 println!("Sequence 1: acgtacgt");
468 println!("Sequence 2: acttacgt");
469 let cost = dna_alignment.cost;
470 println!("\nOptimal alignment (cost: {cost}):");
471 let viz = dna_alignment.visualize();
472 println!("{viz}");
473
474 let mut matches = 0;
476 let mut substitutions = 0;
477 let mut indels = 0;
478
479 for op in &dna_alignment.operations {
480 match op {
481 AlignmentOp::Match(_, _) => matches += 1,
482 AlignmentOp::Substitute(_, _) => substitutions += 1,
483 AlignmentOp::Insert(_) | AlignmentOp::Delete(_) => indels += 1,
484 }
485 }
486
487 println!("\nAlignment statistics:");
488 println!(" Matches: {matches}");
489 println!(" Substitutions: {substitutions}");
490 println!(" Insertions/Deletions: {indels}");
491 println!(
492 " Similarity: {:.1}%",
493 (matches as f32 / dna_alignment.operations.len() as f32) * 100.0
494 );
495
496 println!("\n5. Detailed Operation Description:");
498 println!("----------------------------------");
499 println!("Operations for 'hello' -> 'hallo':");
500
501 let hello_alignment = Alignment {
502 operations: vec![
503 AlignmentOp::Match('h', 'h'),
504 AlignmentOp::Substitute('e', 'a'),
505 AlignmentOp::Match('l', 'l'),
506 AlignmentOp::Match('l', 'l'),
507 AlignmentOp::Match('o', 'o'),
508 ],
509 cost: 1.0,
510 source: "hello".to_string(),
511 target: "hallo".to_string(),
512 };
513
514 let desc = hello_alignment.describe();
515 println!("{desc}");
516
517 Ok(())
518}
519
520fn main() -> Result<()> {
521 demonstrate_string_alignment()?;
522
523 println!("\n=== Summary ===");
524 println!("This example showed how to:");
525 println!("- Build FSTs that encode alignment operations in output symbols");
526 println!("- Extract alignment paths from FST shortest paths");
527 println!("- Visualize string transformations with detailed operations");
528 println!("- Handle multiple optimal alignments");
529 println!("- Apply alignment to biological sequences");
530 println!("- Compare FST-based and manual alignment approaches");
531
532 Ok(())
533}