1use rustc_hash::FxHashMap;
21
22use sicada::algorithms::connect::connect;
23use sicada::algorithms::prune::{PruneOptions, prune as prune_fst};
24use sicada::arc::{Arc, ArcLabel, ArcStateId};
25use sicada::error::OpenFstError;
26use sicada::fst::{Fst, MutableFst};
27use sicada::fsts::vector_fst::VectorFst;
28
29use crate::dense::{DenseFst, FromScore};
30use crate::frontier::{DecodeOptions, NO_AUX, Token, prune, relax_cost};
31use crate::lattice_weight::{LatticeArc, LatticeWeight};
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct LatticeDecodeOptions {
36 pub search: DecodeOptions,
38 pub lattice_beam: f32,
46}
47
48impl Default for LatticeDecodeOptions {
49 fn default() -> Self {
50 Self {
51 search: DecodeOptions::default(),
52 lattice_beam: 8.0,
54 }
55 }
56}
57
58impl LatticeDecodeOptions {
59 pub fn exhaustive() -> Self {
64 Self {
65 search: DecodeOptions::exhaustive(),
66 lattice_beam: f32::INFINITY,
67 }
68 }
69}
70
71pub type Lattice<A> = VectorFst<LatticeArc<A>>;
76
77#[inline(always)]
79fn state_of<A: Arc>(aux: u32) -> A::StateId {
80 A::StateId::from_usize(aux as usize)
81}
82
83struct Pending<A: Arc> {
86 from: u32,
87 to: A::StateId,
88 ilabel: A::Label,
89 olabel: A::Label,
90 graph: f32,
91 acoustic: f32,
92}
93
94pub fn lattice_decode<A, G>(
106 graph: &G,
107 dense: &DenseFst<'_, A>,
108 opts: &LatticeDecodeOptions,
109) -> Result<Option<Lattice<A>>, OpenFstError>
110where
111 A: Arc,
112 A::Weight: FromScore,
113 A::StateId: ArcStateId,
114 G: Fst<A>,
115{
116 let Some(start) = graph.start() else {
117 return Ok(None);
118 };
119
120 let mut lattice: Lattice<A> = VectorFst::new();
121 let mut current: FxHashMap<A::StateId, Token> = FxHashMap::default();
122 let mut next: FxHashMap<A::StateId, Token> = FxHashMap::default();
123 let mut queue: Vec<A::StateId> = Vec::new();
124 let mut costs: Vec<f32> = Vec::new();
125 let mut pending: Vec<Pending<A>> = Vec::new();
126
127 current.insert(
128 start,
129 Token {
130 cost: 0.0,
131 aux: NO_AUX,
132 },
133 );
134 settle_epsilons(graph, &mut current, &mut queue, f32::INFINITY)?;
135 allocate(&mut lattice, &mut current);
136 lattice.set_start(state_of::<A>(current[&start].aux));
137 emit_epsilons(graph, ¤t, &mut lattice);
138
139 for t in 0..dense.num_frames() {
140 let frame = dense.frame(t);
141 next.clear();
142 pending.clear();
143
144 for (&state, &token) in ¤t {
145 for arc in graph.arcs(state) {
146 if arc.ilabel() == A::Label::epsilon() {
147 continue;
148 }
149 let Some(column) = dense.column_of(arc.ilabel()) else {
150 return Err(OpenFstError::InvalidOperation(format!(
151 "lattice_decode: the graph has input label {} at state {state:?}, which \
152 names no column of a {}-symbol acoustic matrix",
153 arc.ilabel(),
154 dense.num_symbols()
155 )));
156 };
157 let graph_cost = arc.weight().to_cost();
158 let acoustic = frame[column];
159 relax_cost(
160 &mut next,
161 arc.nextstate(),
162 token.cost + graph_cost + acoustic,
163 NO_AUX,
164 );
165 pending.push(Pending {
168 from: token.aux,
169 to: arc.nextstate(),
170 ilabel: arc.ilabel(),
171 olabel: arc.olabel(),
172 graph: graph_cost,
173 acoustic,
174 });
175 }
176 }
177
178 if next.is_empty() {
179 return Ok(None);
180 }
181 settle_and_prune(graph, &mut next, &mut queue, &mut costs, &opts.search)?;
182 allocate(&mut lattice, &mut next);
183
184 for step in &pending {
185 let Some(to) = next.get(&step.to) else {
186 continue;
187 };
188 lattice.add_arc(
189 state_of::<A>(step.from),
190 LatticeArc::<A>::new(
191 step.ilabel,
192 step.olabel,
193 LatticeWeight::new(step.graph, step.acoustic),
194 state_of::<A>(to.aux),
195 ),
196 );
197 }
198 emit_epsilons(graph, &next, &mut lattice);
199
200 std::mem::swap(&mut current, &mut next);
201 }
202
203 let mut reached_the_end = false;
204 for (&state, &token) in ¤t {
205 let final_cost = graph.final_weight(state).to_cost();
206 if !final_cost.is_finite() {
207 continue;
208 }
209 reached_the_end = true;
210 lattice.set_final(
211 state_of::<A>(token.aux),
212 LatticeWeight::new(final_cost, 0.0),
213 );
214 }
215 if !reached_the_end {
216 return Ok(None);
217 }
218
219 connect(&mut lattice);
223 if lattice.start().is_none() {
224 return Ok(None);
225 }
226
227 if opts.lattice_beam.is_finite() {
228 prune_fst(
229 &mut lattice,
230 &PruneOptions::threshold(LatticeWeight::new(opts.lattice_beam, 0.0)),
231 )?;
232 if lattice.start().is_none() {
233 return Ok(None);
234 }
235 }
236
237 Ok(Some(lattice))
238}
239
240fn allocate<LA: Arc>(lattice: &mut VectorFst<LA>, frontier: &mut FxHashMap<LA::StateId, Token>) {
247 for token in frontier.values_mut() {
248 token.aux = lattice.add_state().as_usize() as u32;
249 }
250}
251
252fn settle_epsilons<A, G>(
257 graph: &G,
258 frontier: &mut FxHashMap<A::StateId, Token>,
259 queue: &mut Vec<A::StateId>,
260 cutoff: f32,
261) -> Result<(), OpenFstError>
262where
263 A: Arc,
264 A::Weight: FromScore,
265 G: Fst<A>,
266{
267 queue.clear();
268 queue.extend(frontier.keys().copied());
269
270 let budget = frontier.len().saturating_mul(64).saturating_add(1024);
271 let mut steps = 0usize;
272
273 while let Some(state) = queue.pop() {
274 steps += 1;
275 if steps > budget {
276 return Err(OpenFstError::InvalidOperation(
277 "lattice_decode: the graph's epsilon arcs do not settle, which means a cycle of \
278 them costs less than nothing"
279 .into(),
280 ));
281 }
282 let token = frontier[&state];
283 for arc in graph.arcs(state) {
284 if arc.ilabel() != A::Label::epsilon() {
285 continue;
286 }
287 let cost = token.cost + arc.weight().to_cost();
288 if cost > cutoff {
289 continue;
290 }
291 if relax_cost(frontier, arc.nextstate(), cost, NO_AUX) {
292 queue.push(arc.nextstate());
293 }
294 }
295 }
296 Ok(())
297}
298
299fn settle_and_prune<A, G>(
301 graph: &G,
302 frontier: &mut FxHashMap<A::StateId, Token>,
303 queue: &mut Vec<A::StateId>,
304 costs: &mut Vec<f32>,
305 opts: &DecodeOptions,
306) -> Result<f32, OpenFstError>
307where
308 A: Arc,
309 A::Weight: FromScore,
310 G: Fst<A>,
311{
312 let cutoff = prune(frontier, opts, costs);
313 settle_epsilons(graph, frontier, queue, cutoff)?;
314 if frontier.len() > opts.max_active {
317 return Ok(prune(frontier, opts, costs));
318 }
319 Ok(cutoff)
320}
321
322fn emit_epsilons<A, G>(graph: &G, frontier: &FxHashMap<A::StateId, Token>, lattice: &mut Lattice<A>)
324where
325 A: Arc,
326 A::Weight: FromScore,
327 G: Fst<A>,
328{
329 for (&state, &token) in frontier {
330 for arc in graph.arcs(state) {
331 if arc.ilabel() != A::Label::epsilon() {
332 continue;
333 }
334 let Some(to) = frontier.get(&arc.nextstate()) else {
335 continue;
336 };
337 lattice.add_arc(
338 state_of::<A>(token.aux),
339 LatticeArc::<A>::new(
340 arc.ilabel(),
341 arc.olabel(),
342 LatticeWeight::new(arc.weight().to_cost(), 0.0),
345 state_of::<A>(to.aux),
346 ),
347 );
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use sicada::algorithms::arcsort::{ILabelCompare, arc_sort};
356 use sicada::algorithms::compose::compose;
357 use sicada::algorithms::shortest_path::{ShortestPathOptions, shortest_path};
358 use sicada::arc::StdArc;
359 use sicada::fst::ExpandedFst;
360 use sicada::fsts::vector_fst::StdVectorFst;
361 use sicada::properties::K_FST_PROPERTIES;
362 use sicada::string::string_fst_to_output_labels;
363 use sicada::weight::Weight;
364 use sicada::weights::float_weight::TropicalWeight;
365
366 use crate::viterbi::viterbi_decode;
367
368 struct Rng(u64);
370
371 impl Rng {
372 fn next(&mut self) -> u64 {
373 self.0 ^= self.0 << 13;
374 self.0 ^= self.0 >> 7;
375 self.0 ^= self.0 << 17;
376 self.0
377 }
378 fn below(&mut self, n: usize) -> usize {
379 (self.next() % n as u64) as usize
380 }
381 fn cost(&mut self) -> f32 {
382 self.below(4096) as f32 / 64.0
383 }
384 }
385
386 fn random_graph(rng: &mut Rng, symbols: usize) -> StdVectorFst {
387 let states = 1 + rng.below(6);
388 let mut graph: StdVectorFst = VectorFst::new();
389 for _ in 0..states {
390 graph.add_state();
391 }
392 graph.set_start(0);
393 for from in 0..states as i32 {
394 for _ in 0..1 + rng.below(4) {
395 let ilabel = if rng.below(4) == 0 {
396 0
397 } else {
398 1 + rng.below(symbols) as i32
399 };
400 let olabel = if rng.below(3) == 0 {
401 0
402 } else {
403 10 * (1 + rng.below(symbols) as i32)
404 };
405 let to = rng.below(states) as i32;
406 graph.add_arc(
407 from,
408 StdArc::new(ilabel, olabel, TropicalWeight(rng.cost()), to),
409 );
410 }
411 if rng.below(3) == 0 {
412 graph.set_final(from, TropicalWeight(rng.cost()));
413 }
414 }
415 graph.properties(K_FST_PROPERTIES, true);
416 graph
417 }
418
419 fn composition(graph: &StdVectorFst, dense: &DenseFst<'_, StdArc>) -> StdVectorFst {
421 let mut sorted = graph.clone();
422 arc_sort(&mut sorted, &ILabelCompare);
423 let mut composed: StdVectorFst = VectorFst::new();
424 compose(dense, &sorted, &mut composed).expect("a composition");
425 composed
426 }
427
428 fn best_of(lattice: &Lattice<StdArc>) -> (Vec<i32>, f32) {
430 let mut best: Lattice<StdArc> = VectorFst::new();
431 shortest_path(lattice, &mut best, &ShortestPathOptions::default()).expect("a best path");
432 let (labels, weight) = string_fst_to_output_labels(&best).expect("a single path");
433 (
434 labels.into_iter().filter(|&l| l != 0).collect(),
435 weight.total(),
436 )
437 }
438
439 fn free_graph() -> StdVectorFst {
440 let mut fst = VectorFst::new();
441 fst.add_state();
442 fst.set_start(0);
443 fst.set_final(0, TropicalWeight::one());
444 for label in 1..=3 {
445 fst.add_arc(0, StdArc::new(label, label * 10, TropicalWeight::one(), 0));
446 }
447 fst.properties(K_FST_PROPERTIES, true);
448 fst
449 }
450
451 #[test]
452 fn its_best_path_is_the_one_the_viterbi_decoder_finds() {
453 let scores = [
454 5.0, 1.0, 9.0, 0.5, 4.0, 4.0, 3.0, 0.25, 3.0,
457 ];
458 let dense = DenseFst::<StdArc>::new(&scores, 3, 3).unwrap();
459 let graph = free_graph();
460
461 let lattice = lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive())
462 .unwrap()
463 .expect("a lattice");
464 let expected = viterbi_decode(&graph, &dense, &DecodeOptions::exhaustive())
465 .unwrap()
466 .expect("a path");
467
468 let (labels, total) = best_of(&lattice);
469 assert_eq!(labels, expected.labels);
470 assert!((total - expected.weight.0).abs() < 1e-5);
471 }
472
473 #[test]
477 fn the_two_costs_stay_apart() {
478 let scores = [
479 5.0, 1.0, 9.0, 0.5, 4.0, 4.0,
481 ];
482 let dense = DenseFst::<StdArc>::new(&scores, 2, 3).unwrap();
483 let mut graph: StdVectorFst = VectorFst::new();
485 graph.add_state();
486 graph.set_start(0);
487 graph.set_final(0, TropicalWeight::one());
488 for label in 1..=3 {
489 graph.add_arc(0, StdArc::new(label, label * 10, TropicalWeight(0.25), 0));
490 }
491 graph.properties(K_FST_PROPERTIES, true);
492
493 let lattice = lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive())
494 .unwrap()
495 .unwrap();
496 let mut best: Lattice<StdArc> = VectorFst::new();
497 shortest_path(&lattice, &mut best, &ShortestPathOptions::default()).unwrap();
498 let (_, weight) = string_fst_to_output_labels(&best).unwrap();
499
500 assert!((weight.graph - 0.5).abs() < 1e-6, "{weight}");
501 assert!((weight.acoustic - 1.5).abs() < 1e-6, "{weight}");
503 assert!((weight.total_scaled(0.5) - (0.5 + 0.75)).abs() < 1e-6);
505 }
506
507 #[test]
512 fn an_unpruned_lattice_is_the_composition() {
513 let symbols = 4;
514 let mut rng = Rng(0x00C0_FFEE_1234_5678);
515 let mut compared = 0;
516
517 for round in 0..200 {
518 let graph = random_graph(&mut rng, symbols);
519 let frames = 1 + rng.below(5);
520 let scores: Vec<f32> = (0..frames * symbols).map(|_| rng.cost()).collect();
521 let dense = DenseFst::<StdArc>::new(&scores, frames, symbols).unwrap();
522
523 let expected = composition(&graph, &dense);
524 let lattice =
525 lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive()).unwrap();
526
527 let Some(lattice) = lattice else {
528 assert!(
529 expected.start().is_none(),
530 "round {round}: no lattice, but the composition has {} states",
531 expected.num_states()
532 );
533 continue;
534 };
535 compared += 1;
536
537 assert_eq!(
538 lattice.num_states(),
539 expected.num_states(),
540 "round {round}: states"
541 );
542 assert_eq!(
543 lattice.count_arcs(),
544 expected.count_arcs(),
545 "round {round}: arcs"
546 );
547
548 let mut best: StdVectorFst = VectorFst::new();
549 shortest_path(&expected, &mut best, &ShortestPathOptions::default()).unwrap();
550 let (labels, weight) = string_fst_to_output_labels(&best).unwrap();
551 let labels: Vec<i32> = labels.into_iter().filter(|&l| l != 0).collect();
552
553 let (mine, total) = best_of(&lattice);
554 assert!(
555 (total - weight.0).abs() < 1e-4,
556 "round {round}: lattice {total} vs composition {}",
557 weight.0
558 );
559 assert_eq!(mine, labels, "round {round}");
560 }
561
562 assert!(
563 compared > 100,
564 "only {compared} rounds had a lattice at all"
565 );
566 }
567
568 #[test]
576 fn pruning_keeps_the_best_path() {
577 let symbols = 4;
578 let mut rng = Rng(0xBEEF_4321_9876);
579 let mut shrank = 0;
580
581 for round in 0..200 {
582 let graph = random_graph(&mut rng, symbols);
583 let frames = 1 + rng.below(5);
584 let scores: Vec<f32> = (0..frames * symbols).map(|_| rng.cost()).collect();
585 let dense = DenseFst::<StdArc>::new(&scores, frames, symbols).unwrap();
586
587 let whole =
588 lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive()).unwrap();
589 let pruned = lattice_decode(
590 &graph,
591 &dense,
592 &LatticeDecodeOptions {
593 search: DecodeOptions::exhaustive(),
594 lattice_beam: 2.0,
595 },
596 )
597 .unwrap();
598
599 match (whole, pruned) {
600 (None, None) => {}
601 (Some(whole), Some(pruned)) => {
602 assert!(
603 pruned.count_arcs() <= whole.count_arcs(),
604 "round {round}: pruning grew the lattice"
605 );
606 if pruned.count_arcs() < whole.count_arcs() {
607 shrank += 1;
608 }
609 let (whole_labels, whole_cost) = best_of(&whole);
610 let (pruned_labels, pruned_cost) = best_of(&pruned);
611 assert!(
612 (pruned_cost - whole_cost).abs() < 1e-4,
613 "round {round}: {pruned_cost} vs {whole_cost}, {pruned_labels:?} vs {whole_labels:?}"
614 );
615 }
616 (whole, pruned) => panic!(
617 "round {round}: whole {:?}, pruned {:?}",
618 whole.is_some(),
619 pruned.is_some()
620 ),
621 }
622 }
623
624 assert!(shrank > 20, "the beam never removed anything in {shrank}");
625 }
626
627 #[test]
628 fn a_graph_that_reaches_no_final_state_has_no_lattice() {
629 let mut graph: StdVectorFst = VectorFst::new();
630 graph.add_state();
631 graph.set_start(0);
632 graph.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 0));
633 graph.properties(K_FST_PROPERTIES, true);
634
635 let scores = [1.0, 1.0, 1.0];
636 let dense = DenseFst::<StdArc>::new(&scores, 1, 3).unwrap();
637 assert!(
638 lattice_decode(&graph, &dense, &LatticeDecodeOptions::exhaustive())
639 .unwrap()
640 .is_none()
641 );
642 }
643
644 #[test]
647 fn the_input_labels_are_still_the_acoustic_columns() {
648 let scores = [5.0, 1.0, 9.0];
649 let dense = DenseFst::<StdArc>::new(&scores, 1, 3).unwrap();
650 let lattice = lattice_decode(&free_graph(), &dense, &LatticeDecodeOptions::exhaustive())
651 .unwrap()
652 .unwrap();
653
654 for state in lattice.states() {
655 for arc in lattice.arcs(state) {
656 assert_eq!(
657 arc.olabel(),
658 arc.ilabel() * 10,
659 "the graph maps label n to output 10n"
660 );
661 assert!(dense.column_of(arc.ilabel()).is_some());
662 }
663 }
664 }
665}