use crate::arc::Arc;
use crate::fst::{Fst, MutableFst, StateId};
use crate::semiring::{NaturallyOrderedSemiring, Semiring};
use crate::Result;
use rustc_hash::{FxHashMap, FxHashSet};
use std::cmp::Ordering;
use std::collections::{BinaryHeap, VecDeque};
#[derive(Debug, Clone)]
pub struct PruneConfig {
pub weight_threshold: f64,
pub state_threshold: Option<usize>,
pub npath: Option<usize>,
pub use_forward_backward: bool,
pub delta: f64,
}
impl Default for PruneConfig {
fn default() -> Self {
Self {
weight_threshold: f64::INFINITY,
state_threshold: None,
npath: None,
use_forward_backward: false,
delta: 1e-6,
}
}
}
#[derive(Debug, Clone)]
struct PriorityState<W> {
state: StateId,
weight: W,
}
impl<W: PartialOrd> PartialEq for PriorityState<W> {
fn eq(&self, other: &Self) -> bool {
self.weight == other.weight
}
}
impl<W: PartialOrd> Eq for PriorityState<W> {}
impl<W: PartialOrd> Ord for PriorityState<W> {
fn cmp(&self, other: &Self) -> Ordering {
other
.weight
.partial_cmp(&self.weight)
.unwrap_or(Ordering::Equal)
}
}
impl<W: PartialOrd> PartialOrd for PriorityState<W> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn prune<W, F, M>(fst: &F, config: PruneConfig) -> Result<M>
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
F: Fst<W>,
M: MutableFst<W> + Default,
{
if fst.num_states() == 0 || fst.start().is_none() {
return Ok(M::default());
}
if config.use_forward_backward {
prune_forward_backward(fst, &config)
} else if let Some(npath) = config.npath {
prune_nbest_paths(fst, npath, &config)
} else {
prune_by_weight(fst, &config)
}
}
fn prune_forward_backward<W, F, M>(fst: &F, config: &PruneConfig) -> Result<M>
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
F: Fst<W>,
M: MutableFst<W> + Default,
{
let forward_weights = compute_forward_weights(fst)?;
let backward_weights = compute_backward_weights(fst)?;
let mut best_weight = None;
if let Some(start) = fst.start() {
if let Some(backward) = backward_weights.get(&start) {
best_weight = Some(backward.clone());
}
}
let mut result = M::default();
let mut state_map = FxHashMap::default();
let zero_weight = W::zero();
for state in fst.states() {
let forward = forward_weights.get(&state).unwrap_or(&zero_weight);
let backward = backward_weights.get(&state).unwrap_or(&zero_weight);
if *forward == W::zero() || *backward == W::zero() {
continue; }
let total = forward.times(backward);
if should_keep_weight(&total, &best_weight, config) {
let new_state = result.add_state();
state_map.insert(state, new_state);
if let Some(weight) = fst.final_weight(state) {
result.set_final(new_state, weight.clone());
}
}
}
if let Some(start) = fst.start() {
if let Some(&new_start) = state_map.get(&start) {
result.set_start(new_start);
}
}
for (&old_state, &new_state) in &state_map {
for arc in fst.arcs(old_state) {
if let Some(&new_nextstate) = state_map.get(&arc.nextstate) {
let arc_total = if let (Some(forward), Some(backward)) = (
forward_weights.get(&old_state),
backward_weights.get(&arc.nextstate),
) {
forward.times(&arc.weight).times(backward)
} else {
continue;
};
if should_keep_weight(&arc_total, &best_weight, config) {
result.add_arc(
new_state,
Arc::new(arc.ilabel, arc.olabel, arc.weight.clone(), new_nextstate),
);
}
}
}
}
apply_state_threshold(result, config)
}
fn prune_nbest_paths<W, F, M>(fst: &F, npath: usize, config: &PruneConfig) -> Result<M>
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
F: Fst<W>,
M: MutableFst<W> + Default,
{
if npath == 0 {
return Ok(M::default());
}
let start = fst
.start()
.ok_or_else(|| crate::Error::Algorithm("FST has no start state".into()))?;
let mut heap = BinaryHeap::new();
let mut best_weights: FxHashMap<StateId, Vec<W>> = FxHashMap::default();
heap.push(PriorityState {
state: start,
weight: W::one(),
});
best_weights.insert(start, vec![W::one()]);
while let Some(PriorityState { state, weight }) = heap.pop() {
if let Some(final_weight) = fst.final_weight(state) {
let _total = weight.times(final_weight);
}
for arc in fst.arcs(state) {
let new_weight = weight.times(&arc.weight);
let weights = best_weights.entry(arc.nextstate).or_default();
if weights.len() < npath {
weights.push(new_weight.clone());
weights.sort();
if weights.len() > npath {
weights.truncate(npath);
}
heap.push(PriorityState {
state: arc.nextstate,
weight: new_weight,
});
} else if weights.last().is_some_and(|w| new_weight < *w) {
weights[npath - 1] = new_weight.clone();
weights.sort();
heap.push(PriorityState {
state: arc.nextstate,
weight: new_weight,
});
}
}
}
build_nbest_fst(fst, &best_weights, npath, config)
}
fn prune_by_weight<W, F, M>(fst: &F, config: &PruneConfig) -> Result<M>
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
F: Fst<W>,
M: MutableFst<W> + Default,
{
let mut result = M::default();
let mut state_map = FxHashMap::default();
let distances = compute_shortest_distances(fst)?;
for state in fst.states() {
if let Some(distance) = distances.get(&state) {
if convert_weight_to_f64(distance) <= config.weight_threshold {
let new_state = result.add_state();
state_map.insert(state, new_state);
if let Some(weight) = fst.final_weight(state) {
result.set_final(new_state, weight.clone());
}
}
}
}
if let Some(start) = fst.start() {
if let Some(&new_start) = state_map.get(&start) {
result.set_start(new_start);
}
}
for (&old_state, &new_state) in &state_map {
if let Some(state_distance) = distances.get(&old_state) {
for arc in fst.arcs(old_state) {
if let Some(&new_nextstate) = state_map.get(&arc.nextstate) {
let arc_distance = state_distance.times(&arc.weight);
if convert_weight_to_f64(&arc_distance) <= config.weight_threshold {
result.add_arc(
new_state,
Arc::new(arc.ilabel, arc.olabel, arc.weight.clone(), new_nextstate),
);
}
}
}
}
}
apply_state_threshold(result, config)
}
fn compute_forward_weights<W, F>(fst: &F) -> Result<FxHashMap<StateId, W>>
where
W: NaturallyOrderedSemiring,
F: Fst<W>,
{
let mut weights = FxHashMap::default();
let mut queue = VecDeque::new();
let mut in_queue = FxHashSet::default();
if let Some(start) = fst.start() {
weights.insert(start, W::one());
queue.push_back(start);
in_queue.insert(start);
}
let num_states = fst.num_states();
let max_iterations = num_states;
let mut iterations = 0;
while let Some(state) = queue.pop_front() {
in_queue.remove(&state);
iterations += 1;
if iterations > max_iterations {
break;
}
let state_weight = weights[&state].clone();
for arc in fst.arcs(state) {
let new_weight = state_weight.times(&arc.weight);
let updated = match weights.get(&arc.nextstate) {
None => {
weights.insert(arc.nextstate, new_weight);
true
}
Some(old_weight) => {
if new_weight < *old_weight {
weights.insert(arc.nextstate, new_weight);
true
} else {
false
}
}
};
if updated && !in_queue.contains(&arc.nextstate) {
queue.push_back(arc.nextstate);
in_queue.insert(arc.nextstate);
}
}
}
Ok(weights)
}
fn compute_backward_weights<W, F>(fst: &F) -> Result<FxHashMap<StateId, W>>
where
W: NaturallyOrderedSemiring,
F: Fst<W>,
{
let mut weights = FxHashMap::default();
let mut reverse_arcs: FxHashMap<StateId, Vec<(StateId, W)>> = FxHashMap::default();
for state in fst.states() {
for arc in fst.arcs(state) {
reverse_arcs
.entry(arc.nextstate)
.or_default()
.push((state, arc.weight.clone()));
}
}
let mut queue = VecDeque::new();
let mut in_queue = FxHashSet::default();
for state in fst.states() {
if let Some(final_weight) = fst.final_weight(state) {
weights.insert(state, final_weight.clone());
queue.push_back(state);
in_queue.insert(state);
}
}
let num_states = fst.num_states();
let max_iterations = num_states;
let mut iterations = 0;
while let Some(state) = queue.pop_front() {
in_queue.remove(&state);
iterations += 1;
if iterations > max_iterations {
break;
}
let state_weight = weights[&state].clone();
if let Some(predecessors) = reverse_arcs.get(&state) {
for (prev_state, arc_weight) in predecessors {
let new_weight = arc_weight.times(&state_weight);
let updated = match weights.get(prev_state) {
None => {
weights.insert(*prev_state, new_weight);
true
}
Some(old_weight) => {
let combined = old_weight.plus(&new_weight);
if combined != *old_weight {
weights.insert(*prev_state, combined);
true
} else {
false
}
}
};
if updated && !in_queue.contains(prev_state) {
queue.push_back(*prev_state);
in_queue.insert(*prev_state);
}
}
}
}
Ok(weights)
}
fn compute_shortest_distances<W, F>(fst: &F) -> Result<FxHashMap<StateId, W>>
where
W: NaturallyOrderedSemiring,
F: Fst<W>,
{
compute_forward_weights(fst)
}
fn build_nbest_fst<W, F, M>(
fst: &F,
best_weights: &FxHashMap<StateId, Vec<W>>,
_npath: usize,
_config: &PruneConfig,
) -> Result<M>
where
W: NaturallyOrderedSemiring,
F: Fst<W>,
M: MutableFst<W> + Default,
{
let mut result = M::default();
let mut state_map = FxHashMap::default();
for &state in best_weights.keys() {
let new_state = result.add_state();
state_map.insert(state, new_state);
if let Some(weight) = fst.final_weight(state) {
result.set_final(new_state, weight.clone());
}
}
if let Some(start) = fst.start() {
if let Some(&new_start) = state_map.get(&start) {
result.set_start(new_start);
}
}
for (&state, &new_state) in &state_map {
for arc in fst.arcs(state) {
if let Some(&new_nextstate) = state_map.get(&arc.nextstate) {
result.add_arc(
new_state,
Arc::new(arc.ilabel, arc.olabel, arc.weight.clone(), new_nextstate),
);
}
}
}
Ok(result)
}
fn apply_state_threshold<W, M>(fst: M, config: &PruneConfig) -> Result<M>
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
M: MutableFst<W> + Default,
{
let threshold = match config.state_threshold {
Some(t) => t,
None => return Ok(fst),
};
if fst.num_states() <= threshold {
return Ok(fst);
}
let start = match fst.start() {
Some(s) => s,
None => return Ok(fst), };
let forward_weights = compute_forward_weights(&fst)?;
let backward_weights = compute_backward_weights(&fst)?;
let mut state_importance: Vec<(StateId, f64)> = Vec::new();
for state in fst.states() {
let forward = forward_weights.get(&state);
let backward = backward_weights.get(&state);
match (forward, backward) {
(Some(fw), Some(bw)) => {
if Semiring::is_zero(fw) || Semiring::is_zero(bw) {
continue;
}
let importance = fw.times(bw);
let importance_val = convert_weight_to_f64(&importance);
if importance_val.is_finite() {
state_importance.push((state, importance_val));
}
}
_ => {
}
}
}
if state_importance.is_empty() {
return Ok(M::default());
}
state_importance.sort_by(|a, b| match a.1.partial_cmp(&b.1) {
Some(std::cmp::Ordering::Equal) | None => a.0.cmp(&b.0),
Some(ord) => ord,
});
let mut selected_states: FxHashSet<StateId> = FxHashSet::default();
let start_in_importance = state_importance.iter().any(|(s, _)| *s == start);
if !start_in_importance {
return Ok(M::default());
}
selected_states.insert(start);
for (state, _) in &state_importance {
if selected_states.len() >= threshold {
break;
}
selected_states.insert(*state);
}
let mut result = M::default();
let mut state_map: FxHashMap<StateId, StateId> = FxHashMap::default();
for &old_state in &selected_states {
let new_state = result.add_state();
state_map.insert(old_state, new_state);
}
if let Some(&new_start) = state_map.get(&start) {
result.set_start(new_start);
}
for &old_state in &selected_states {
let new_state = match state_map.get(&old_state) {
Some(&s) => s,
None => continue,
};
if let Some(weight) = fst.final_weight(old_state) {
result.set_final(new_state, weight.clone());
}
for arc in fst.arcs(old_state) {
if let Some(&new_nextstate) = state_map.get(&arc.nextstate) {
result.add_arc(
new_state,
Arc::new(arc.ilabel, arc.olabel, arc.weight.clone(), new_nextstate),
);
}
}
}
Ok(result)
}
fn should_keep_weight<W>(weight: &W, best: &Option<W>, config: &PruneConfig) -> bool
where
W: NaturallyOrderedSemiring,
W::Value: Into<f64> + Copy,
{
if convert_weight_to_f64(weight) > config.weight_threshold {
return false;
}
if let Some(best_weight) = best {
let weight_val = convert_weight_to_f64(weight);
let best_val = convert_weight_to_f64(best_weight);
if weight_val > best_val + config.weight_threshold {
return false;
}
}
true
}
fn convert_weight_to_f64<W>(weight: &W) -> f64
where
W: Semiring,
W::Value: Into<f64> + Copy,
{
(*weight.value()).into()
}
#[allow(dead_code)]
fn compute_reachable_states<F: Fst<W>, W: Semiring>(fst: &F, start: StateId) -> FxHashSet<StateId> {
let mut reachable = FxHashSet::default();
let mut stack = vec![start];
while let Some(state) = stack.pop() {
if reachable.insert(state) {
for arc in fst.arcs(state) {
stack.push(arc.nextstate);
}
}
}
reachable
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn test_prune_config_default() {
let config = PruneConfig::default();
assert_eq!(config.weight_threshold, f64::INFINITY);
assert_eq!(config.state_threshold, None);
assert_eq!(config.npath, None);
assert!(!config.use_forward_backward);
}
#[test]
fn test_prune_config_custom() {
let config = PruneConfig {
weight_threshold: 5.0,
state_threshold: Some(100),
npath: Some(10),
use_forward_backward: true,
delta: 1e-8,
};
assert_eq!(config.weight_threshold, 5.0);
assert_eq!(config.state_threshold, Some(100));
assert_eq!(config.npath, Some(10));
assert!(config.use_forward_backward);
}
#[test]
fn test_prune_simple_fst() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(2.0), s2));
let config = PruneConfig {
weight_threshold: 10.0,
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.num_states() > 0);
assert!(pruned.start().is_some());
}
#[test]
fn test_prune_weighted_paths() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(0.5), s1));
fst.add_arc(s0, Arc::new(2, 2, TropicalWeight::new(5.0), s2));
let config = PruneConfig {
weight_threshold: 3.0,
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.start().is_some());
assert!(pruned.num_states() > 0);
if let Some(start) = pruned.start() {
let reachable = compute_reachable_states(&pruned, start);
assert!(reachable.iter().any(|&s| pruned.is_final(s)));
}
}
#[test]
fn test_prune_empty_fst() {
let fst = VectorFst::<TropicalWeight>::new();
let config = PruneConfig::default();
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 0);
assert!(pruned.is_empty());
}
#[test]
fn test_prune_single_state() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
fst.set_start(s0);
fst.set_final(s0, TropicalWeight::new(2.0));
let config = PruneConfig {
weight_threshold: 5.0,
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 1);
assert!(pruned.start().is_some());
}
#[test]
fn test_prune_forward_backward() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s2));
fst.add_arc(s0, Arc::new(3, 3, TropicalWeight::new(10.0), s2));
let config = PruneConfig {
weight_threshold: 5.0,
use_forward_backward: true,
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.start().is_some());
assert!(pruned.num_states() > 0);
}
#[test]
fn test_prune_nbest() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s3, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(0.5), s3));
fst.add_arc(s0, Arc::new(3, 3, TropicalWeight::new(2.0), s2));
fst.add_arc(s2, Arc::new(4, 4, TropicalWeight::new(0.5), s3));
let config = PruneConfig {
npath: Some(1),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.start().is_some());
assert!(pruned.num_states() > 0);
}
#[test]
fn test_convert_weight_to_f64() {
let w1 = TropicalWeight::new(3.5);
let val1 = convert_weight_to_f64(&w1);
assert!((val1 - 3.5).abs() < 1e-6);
let w2 = TropicalWeight::zero();
let val2 = convert_weight_to_f64(&w2);
assert_eq!(val2, f64::INFINITY);
}
#[test]
fn test_priority_state() {
let ps1 = PriorityState {
state: 0,
weight: TropicalWeight::new(1.0),
};
let ps2 = PriorityState {
state: 1,
weight: TropicalWeight::new(2.0),
};
assert!(ps1 > ps2);
}
#[test]
fn test_prune_with_state_threshold() {
let mut fst = VectorFst::<TropicalWeight>::new();
let states: Vec<_> = (0..10).map(|_| fst.add_state()).collect();
fst.set_start(states[0]);
fst.set_final(states[9], TropicalWeight::one());
for i in 0..9 {
fst.add_arc(
states[i],
Arc::new(
(i + 1) as u32,
(i + 1) as u32,
TropicalWeight::new(0.1),
states[i + 1],
),
);
}
let config = PruneConfig {
state_threshold: Some(5),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.num_states() > 0);
}
#[test]
fn test_prune_complex_graph() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s3, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s0, Arc::new(2, 2, TropicalWeight::new(2.0), s2));
fst.add_arc(s1, Arc::new(3, 3, TropicalWeight::new(1.0), s3));
fst.add_arc(s2, Arc::new(4, 4, TropicalWeight::new(1.0), s3));
let config = PruneConfig {
weight_threshold: 2.5,
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.start().is_some());
if let Some(start) = pruned.start() {
let reachable = compute_reachable_states(&pruned, start);
assert!(reachable.iter().any(|&s| pruned.is_final(s)));
}
}
#[test]
fn test_state_threshold_limits_states() {
let mut fst = VectorFst::<TropicalWeight>::new();
let states: Vec<_> = (0..10).map(|_| fst.add_state()).collect();
fst.set_start(states[0]);
fst.set_final(states[9], TropicalWeight::one());
for i in 0..9 {
fst.add_arc(
states[i],
Arc::new(
(i + 1) as u32,
(i + 1) as u32,
TropicalWeight::new(1.0),
states[i + 1],
),
);
}
let config = PruneConfig {
state_threshold: Some(5),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(
pruned.num_states() <= 5,
"Expected at most 5 states, got {}",
pruned.num_states()
);
assert!(pruned.start().is_some());
}
#[test]
fn test_state_threshold_keeps_best_states() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state(); let s1 = fst.add_state(); let s2 = fst.add_state(); let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s3, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s3));
fst.add_arc(s0, Arc::new(3, 3, TropicalWeight::new(5.0), s2));
fst.add_arc(s2, Arc::new(4, 4, TropicalWeight::new(5.0), s3));
let config = PruneConfig {
state_threshold: Some(3),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 3);
assert!(pruned.start().is_some());
let start = pruned.start().unwrap();
let reachable = compute_reachable_states(&pruned, start);
assert!(
reachable.iter().any(|&s| pruned.is_final(s)),
"Pruned FST should still have a path to a final state"
);
}
#[test]
fn test_state_threshold_preserves_final_weights() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::new(0.5));
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
let config = PruneConfig {
state_threshold: Some(5), ..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 2);
let mut has_final = false;
for state in pruned.states() {
if let Some(w) = pruned.final_weight(state) {
has_final = true;
assert_eq!(*w.value(), 0.5);
}
}
assert!(has_final, "Should have preserved final state with weight");
}
#[test]
fn test_state_threshold_excludes_dead_ends() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state(); let s1 = fst.add_state(); let s2 = fst.add_state(); let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s3, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s3));
fst.add_arc(s0, Arc::new(3, 3, TropicalWeight::new(0.1), s2));
let config = PruneConfig {
state_threshold: Some(3),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 3);
let start = pruned.start().unwrap();
let reachable = compute_reachable_states(&pruned, start);
assert!(reachable.iter().any(|&s| pruned.is_final(s)));
}
#[test]
fn test_state_threshold_with_multiple_finals() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::new(0.5)); fst.set_final(s3, TropicalWeight::new(2.0));
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(0.5), s2));
fst.add_arc(s1, Arc::new(3, 3, TropicalWeight::new(2.5), s3));
let config = PruneConfig {
state_threshold: Some(3),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.num_states() <= 3);
assert!(pruned.start().is_some());
}
#[test]
fn test_state_threshold_no_start() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
fst.set_final(s0, TropicalWeight::one());
let config = PruneConfig {
state_threshold: Some(1),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.start().is_none() || pruned.num_states() == 0);
}
#[test]
fn test_state_threshold_all_states_on_best_path() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s2));
let config = PruneConfig {
state_threshold: Some(2),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(pruned.num_states() <= 2);
}
#[test]
fn test_state_threshold_below_current_count() {
let mut fst = VectorFst::<TropicalWeight>::new();
let states: Vec<_> = (0..20).map(|_| fst.add_state()).collect();
fst.set_start(states[0]);
fst.set_final(states[19], TropicalWeight::one());
for i in 0..19 {
fst.add_arc(
states[i],
Arc::new(1, 1, TropicalWeight::new(0.5), states[i + 1]),
);
}
let config = PruneConfig {
state_threshold: Some(5),
..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert!(
pruned.num_states() <= 5,
"Expected at most 5 states, got {}",
pruned.num_states()
);
}
#[test]
fn test_state_threshold_equal_to_current() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
let config = PruneConfig {
state_threshold: Some(2), ..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 2);
}
#[test]
fn test_state_threshold_larger_than_current() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
let config = PruneConfig {
state_threshold: Some(100), ..Default::default()
};
let pruned: VectorFst<TropicalWeight> = prune(&fst, config).unwrap();
assert_eq!(pruned.num_states(), 2);
}
}