1use rustc_hash::FxHashMap;
23
24use sicada::arc::{Arc, ArcLabel};
25use sicada::error::OpenFstError;
26use sicada::fst::Fst;
27use sicada::weight::PathWeight;
28
29use crate::dense::{DenseFst, FromScore};
30use crate::frontier::{DecodeOptions, NO_AUX, Token, prune};
31
32#[derive(Debug, Clone, PartialEq)]
34pub struct Decoded<A: Arc> {
35 pub labels: Vec<A::Label>,
37 pub weight: A::Weight,
40}
41
42#[derive(Debug, Clone, Copy)]
48struct Link<L> {
49 prev: u32,
50 olabel: L,
51}
52
53pub fn viterbi_decode<A, G>(
68 graph: &G,
69 dense: &DenseFst<'_, A>,
70 opts: &DecodeOptions,
71) -> Result<Option<Decoded<A>>, OpenFstError>
72where
73 A: Arc,
74 A::Weight: FromScore + PathWeight,
75 G: Fst<A>,
76{
77 let Some(start) = graph.start() else {
78 return Ok(None);
79 };
80
81 let mut links: Vec<Link<A::Label>> = Vec::new();
82 let mut current: FxHashMap<A::StateId, Token> = FxHashMap::default();
83 let mut next: FxHashMap<A::StateId, Token> = FxHashMap::default();
84 let mut queue: Vec<A::StateId> = Vec::new();
85 let mut costs: Vec<f32> = Vec::new();
86
87 current.insert(
88 start,
89 Token {
90 cost: 0.0,
91 aux: NO_AUX,
92 },
93 );
94 relax_epsilons(graph, &mut current, &mut links, &mut queue, f32::INFINITY)?;
95
96 for t in 0..dense.num_frames() {
97 let frame = dense.frame(t);
98 next.clear();
99
100 for (&state, &token) in ¤t {
101 for arc in graph.arcs(state) {
102 if arc.ilabel() == A::Label::epsilon() {
103 continue;
104 }
105 let Some(column) = dense.column_of(arc.ilabel()) else {
106 return Err(OpenFstError::InvalidOperation(format!(
107 "viterbi_decode: the graph has input label {} at state {state:?}, which \
108 names no column of a {}-symbol acoustic matrix",
109 arc.ilabel(),
110 dense.num_symbols()
111 )));
112 };
113 let cost = token.cost + arc.weight().to_cost() + frame[column];
114 relax(
115 &mut next,
116 &mut links,
117 arc.nextstate(),
118 cost,
119 token.aux,
120 arc.olabel(),
121 );
122 }
123 }
124
125 if next.is_empty() {
126 return Ok(None);
127 }
128 let cutoff = prune(&mut next, opts, &mut costs);
129 relax_epsilons(graph, &mut next, &mut links, &mut queue, cutoff)?;
130 if next.len() > opts.max_active {
133 prune(&mut next, opts, &mut costs);
134 }
135
136 std::mem::swap(&mut current, &mut next);
137 }
138
139 let mut best: Option<(f32, u32)> = None;
140 for (&state, &token) in ¤t {
141 let final_cost = graph.final_weight(state).to_cost();
142 if !final_cost.is_finite() {
143 continue;
144 }
145 let total = token.cost + final_cost;
146 if best.is_none_or(|(so_far, _)| total < so_far) {
147 best = Some((total, token.aux));
148 }
149 }
150
151 Ok(best.map(|(total, link)| Decoded {
152 labels: trace_back(&links, link),
153 weight: A::Weight::from_cost(total),
154 }))
155}
156
157#[inline]
164fn relax<S, L>(
165 frontier: &mut FxHashMap<S, Token>,
166 links: &mut Vec<Link<L>>,
167 state: S,
168 cost: f32,
169 prev_link: u32,
170 olabel: L,
171) -> bool
172where
173 S: std::hash::Hash + Eq,
174 L: ArcLabel,
175{
176 match frontier.get_mut(&state) {
177 Some(token) if token.cost <= cost => false,
178 slot => {
179 let link = if olabel == L::epsilon() {
182 prev_link
183 } else {
184 links.push(Link {
185 prev: prev_link,
186 olabel,
187 });
188 (links.len() - 1) as u32
189 };
190 let token = Token { cost, aux: link };
191 match slot {
192 Some(existing) => *existing = token,
193 None => {
194 frontier.insert(state, token);
195 }
196 }
197 true
198 }
199 }
200}
201
202fn relax_epsilons<A, G>(
210 graph: &G,
211 frontier: &mut FxHashMap<A::StateId, Token>,
212 links: &mut Vec<Link<A::Label>>,
213 queue: &mut Vec<A::StateId>,
214 cutoff: f32,
215) -> Result<(), OpenFstError>
216where
217 A: Arc,
218 A::Weight: FromScore,
219 G: Fst<A>,
220{
221 queue.clear();
222 queue.extend(frontier.keys().copied());
223
224 let budget = frontier.len().saturating_mul(64).saturating_add(1024);
227 let mut steps = 0usize;
228
229 while let Some(state) = queue.pop() {
230 steps += 1;
231 if steps > budget {
232 return Err(OpenFstError::InvalidOperation(
233 "viterbi_decode: the graph's epsilon arcs do not settle, which means a cycle of \
234 them costs less than nothing"
235 .into(),
236 ));
237 }
238 let token = frontier[&state];
239 for arc in graph.arcs(state) {
240 if arc.ilabel() != A::Label::epsilon() {
241 continue;
242 }
243 let cost = token.cost + arc.weight().to_cost();
244 if cost > cutoff {
245 continue;
246 }
247 if relax(
248 frontier,
249 links,
250 arc.nextstate(),
251 cost,
252 token.aux,
253 arc.olabel(),
254 ) {
255 queue.push(arc.nextstate());
256 }
257 }
258 }
259 Ok(())
260}
261
262fn trace_back<L: Copy>(links: &[Link<L>], mut link: u32) -> Vec<L> {
264 let mut labels = Vec::new();
265 while link != NO_AUX {
266 let step = links[link as usize];
267 labels.push(step.olabel);
268 link = step.prev;
269 }
270 labels.reverse();
271 labels
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use sicada::algorithms::arcsort::{ILabelCompare, arc_sort};
278 use sicada::algorithms::compose::compose;
279 use sicada::algorithms::shortest_path::{ShortestPathOptions, shortest_path};
280 use sicada::arc::StdArc;
281 use sicada::fst::MutableFst;
282 use sicada::fsts::vector_fst::{StdVectorFst, VectorFst};
283 use sicada::properties::K_FST_PROPERTIES;
284 use sicada::string::string_fst_to_output_labels;
285 use sicada::weight::Weight;
286 use sicada::weights::float_weight::TropicalWeight;
287
288 fn by_composition(
295 graph: &StdVectorFst,
296 dense: &DenseFst<'_, StdArc>,
297 ) -> Option<(Vec<i32>, f32)> {
298 let mut sorted = graph.clone();
303 arc_sort(&mut sorted, &ILabelCompare);
304 let mut composed: StdVectorFst = VectorFst::new();
305 compose(dense, &sorted, &mut composed).expect("a composition");
306 composed.start()?;
307 let mut best: StdVectorFst = VectorFst::new();
308 shortest_path(&composed, &mut best, &ShortestPathOptions::default()).expect("a best path");
309 best.start()?;
310 let (labels, weight) = string_fst_to_output_labels(&best).expect("a single path");
311 Some((labels.into_iter().filter(|&l| l != 0).collect(), weight.0))
314 }
315
316 fn free_graph() -> StdVectorFst {
319 let mut fst = VectorFst::new();
320 fst.add_state();
321 fst.set_start(0);
322 fst.set_final(0, TropicalWeight::one());
323 for label in 1..=3 {
324 fst.add_arc(0, StdArc::new(label, label * 10, TropicalWeight::one(), 0));
325 }
326 fst.properties(K_FST_PROPERTIES, true);
327 fst
328 }
329
330 #[test]
331 fn it_picks_the_best_symbol_in_every_frame() {
332 let scores = [
334 5.0, 1.0, 9.0, 0.5, 4.0, 4.0, 3.0, 0.25, 3.0,
337 ];
338 let dense = DenseFst::<StdArc>::new(&scores, 3, 3).unwrap();
339 let decoded = viterbi_decode(&free_graph(), &dense, &DecodeOptions::exhaustive())
340 .unwrap()
341 .expect("a path");
342
343 assert_eq!(decoded.labels, vec![20, 10, 20]);
344 assert!((decoded.weight.0 - (1.0 + 0.5 + 0.25)).abs() < 1e-6);
345 }
346
347 #[test]
348 fn it_agrees_with_composing_and_taking_the_shortest_path() {
349 let scores = [
350 5.0, 1.0, 9.0, 0.5, 4.0, 4.0, 3.0, 0.25, 3.0, 2.0, 2.5, 0.75,
354 ];
355 let dense = DenseFst::<StdArc>::new(&scores, 4, 3).unwrap();
356 let graph = free_graph();
357
358 let (labels, weight) = by_composition(&graph, &dense).expect("an answer");
359 let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
360 .unwrap()
361 .expect("a path");
362
363 assert_eq!(decoded.labels, labels);
364 assert!((decoded.weight.0 - weight).abs() < 1e-5);
365 }
366
367 #[test]
370 fn it_agrees_on_a_graph_that_forbids_repeats() {
371 let mut graph: StdVectorFst = VectorFst::new();
373 for _ in 0..4 {
374 graph.add_state();
375 }
376 graph.set_start(0);
377 for from in 0..4 {
378 for label in 1..=3i32 {
379 if from == label {
380 continue;
381 }
382 graph.add_arc(
383 from,
384 StdArc::new(label, label * 10, TropicalWeight(label as f32 * 0.1), label),
385 );
386 }
387 }
388 for state in 1..4 {
389 graph.set_final(state, TropicalWeight(0.5));
390 }
391 graph.properties(K_FST_PROPERTIES, true);
392
393 let scores = [
394 5.0, 1.0, 9.0, 0.5, 4.0, 4.0, 3.0, 0.25, 3.0, 2.0, 2.5, 0.75, 1.0, 1.0, 1.0,
399 ];
400 let dense = DenseFst::<StdArc>::new(&scores, 5, 3).unwrap();
401
402 let (labels, weight) = by_composition(&graph, &dense).expect("an answer");
403 let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
404 .unwrap()
405 .expect("a path");
406
407 assert_eq!(decoded.labels, labels);
408 assert!((decoded.weight.0 - weight).abs() < 1e-5);
409 }
410
411 #[test]
415 fn it_agrees_when_the_graph_has_epsilon_arcs() {
416 let mut graph: StdVectorFst = VectorFst::new();
417 for _ in 0..3 {
418 graph.add_state();
419 }
420 graph.set_start(0);
421 graph.set_final(2, TropicalWeight::one());
422 graph.add_arc(0, StdArc::new(1, 10, TropicalWeight::one(), 0));
424 graph.add_arc(0, StdArc::new(2, 20, TropicalWeight(0.2), 0));
425 graph.add_arc(0, StdArc::new(0, 0, TropicalWeight(0.3), 1));
426 graph.add_arc(1, StdArc::new(0, 99, TropicalWeight(0.4), 2));
427 graph.add_arc(2, StdArc::new(1, 10, TropicalWeight::one(), 0));
428 graph.properties(K_FST_PROPERTIES, true);
429
430 let scores = [
431 0.5, 2.0, 9.0, 2.0, 0.5, 9.0, 0.5, 2.0, 9.0,
434 ];
435 let dense = DenseFst::<StdArc>::new(&scores, 3, 3).unwrap();
436
437 let (labels, weight) = by_composition(&graph, &dense).expect("an answer");
438 let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
439 .unwrap()
440 .expect("a path");
441
442 assert_eq!(decoded.labels, labels);
443 assert!((decoded.weight.0 - weight).abs() < 1e-5);
444 }
445
446 struct Rng(u64);
448
449 impl Rng {
450 fn next(&mut self) -> u64 {
451 self.0 ^= self.0 << 13;
452 self.0 ^= self.0 >> 7;
453 self.0 ^= self.0 << 17;
454 self.0
455 }
456
457 fn below(&mut self, n: usize) -> usize {
458 (self.next() % n as u64) as usize
459 }
460
461 fn cost(&mut self) -> f32 {
464 self.below(4096) as f32 / 64.0
465 }
466 }
467
468 #[test]
470 fn it_agrees_with_the_composition_on_random_graphs() {
471 let symbols = 4;
472 let mut rng = Rng(0x5EED_1234_9ABC_DEF1);
473 let mut compared = 0;
474
475 for round in 0..200 {
476 let states = 1 + rng.below(6);
477 let mut graph: StdVectorFst = VectorFst::new();
478 for _ in 0..states {
479 graph.add_state();
480 }
481 graph.set_start(0);
482 for from in 0..states as i32 {
483 for _ in 0..1 + rng.below(4) {
484 let ilabel = if rng.below(4) == 0 {
486 0
487 } else {
488 1 + rng.below(symbols) as i32
489 };
490 let olabel = if rng.below(3) == 0 {
491 0
492 } else {
493 10 * (1 + rng.below(symbols) as i32)
494 };
495 let to = rng.below(states) as i32;
496 graph.add_arc(
497 from,
498 StdArc::new(ilabel, olabel, TropicalWeight(rng.cost()), to),
499 );
500 }
501 if rng.below(3) == 0 {
502 graph.set_final(from, TropicalWeight(rng.cost()));
503 }
504 }
505 graph.properties(K_FST_PROPERTIES, true);
506
507 let frames = 1 + rng.below(5);
508 let scores: Vec<f32> = (0..frames * symbols).map(|_| rng.cost()).collect();
509 let dense = DenseFst::<StdArc>::new(&scores, frames, symbols).unwrap();
510
511 let expected = by_composition(&graph, &dense);
512 let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive()).unwrap();
513
514 match (expected, decoded) {
515 (None, None) => {}
516 (Some((labels, weight)), Some(decoded)) => {
517 compared += 1;
518 assert!(
519 (decoded.weight.0 - weight).abs() < 1e-4,
520 "round {round}: decoder {} vs composition {weight}",
521 decoded.weight.0
522 );
523 assert_eq!(decoded.labels, labels, "round {round}");
524 }
525 (expected, decoded) => {
526 panic!("round {round}: composition {expected:?}, decoder {decoded:?}")
527 }
528 }
529 }
530
531 assert!(compared > 100, "only {compared} rounds had a path at all");
534 }
535
536 #[test]
537 fn a_beam_that_keeps_the_best_path_does_not_change_the_answer() {
538 let scores = [
539 5.0, 1.0, 9.0, 0.5, 4.0, 4.0, 3.0, 0.25, 3.0,
542 ];
543 let dense = DenseFst::<StdArc>::new(&scores, 3, 3).unwrap();
544 let graph = free_graph();
545
546 let wide = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
547 .unwrap()
548 .unwrap();
549 let narrow = viterbi_decode(
550 &graph,
551 &dense,
552 &DecodeOptions {
553 beam: 0.001,
554 max_active: 1,
555 min_active: 0,
556 },
557 )
558 .unwrap()
559 .unwrap();
560
561 assert_eq!(narrow.labels, wide.labels);
562 assert!((narrow.weight.0 - wide.weight.0).abs() < 1e-6);
563 }
564
565 #[test]
566 fn a_graph_the_model_does_not_match_is_reported() {
567 let mut graph: StdVectorFst = VectorFst::new();
568 graph.add_state();
569 graph.set_start(0);
570 graph.set_final(0, TropicalWeight::one());
571 graph.add_arc(0, StdArc::new(8, 1, TropicalWeight::one(), 0));
573 graph.properties(K_FST_PROPERTIES, true);
574
575 let scores = [1.0, 1.0, 1.0];
576 let dense = DenseFst::<StdArc>::new(&scores, 1, 3).unwrap();
577 let err = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive()).unwrap_err();
578 assert!(format!("{err}").contains("names no column"), "{err}");
579 }
580
581 #[test]
582 fn a_graph_that_reaches_no_final_state_decodes_to_nothing() {
583 let mut graph: StdVectorFst = VectorFst::new();
584 graph.add_state();
585 graph.set_start(0);
586 graph.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 0));
587 graph.properties(K_FST_PROPERTIES, true);
588
589 let scores = [1.0, 1.0, 1.0];
590 let dense = DenseFst::<StdArc>::new(&scores, 1, 3).unwrap();
591 assert_eq!(
592 viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive()).unwrap(),
593 None
594 );
595 }
596
597 #[test]
598 fn an_epsilon_cycle_that_costs_less_than_nothing_is_reported() {
599 let mut graph: StdVectorFst = VectorFst::new();
600 graph.add_state();
601 graph.add_state();
602 graph.set_start(0);
603 graph.set_final(1, TropicalWeight::one());
604 graph.add_arc(0, StdArc::new(0, 0, TropicalWeight(-1.0), 1));
605 graph.add_arc(1, StdArc::new(0, 0, TropicalWeight(-1.0), 0));
606 graph.properties(K_FST_PROPERTIES, true);
607
608 let scores = [1.0, 1.0, 1.0];
609 let dense = DenseFst::<StdArc>::new(&scores, 1, 3).unwrap();
610 let err = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive()).unwrap_err();
611 assert!(format!("{err}").contains("less than nothing"), "{err}");
612 }
613
614 #[test]
615 fn no_frames_decodes_the_graphs_own_best_path() {
616 let graph = free_graph();
617 let dense = DenseFst::<StdArc>::new(&[], 0, 3).unwrap();
618 let decoded = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
619 .unwrap()
620 .expect("the empty path");
621 assert!(decoded.labels.is_empty());
622 assert_eq!(decoded.weight, TropicalWeight::one());
623 }
624}