use crate::arc::Arc;
use crate::fst::{Fst, MutableFst, StateId};
use crate::semiring::Semiring;
use crate::{Error, Result};
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::VecDeque;
pub trait ComposeFilter<W: Semiring> {
type FilterState: Clone + Default + Eq + std::hash::Hash;
fn start() -> Self::FilterState;
fn filter_arc(
&self,
arc1: &Arc<W>,
arc2: &Arc<W>,
fs: &Self::FilterState,
) -> Option<(Arc<W>, Self::FilterState)>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultComposeFilter;
impl<W: Semiring> ComposeFilter<W> for DefaultComposeFilter {
type FilterState = ();
fn start() -> Self::FilterState {}
fn filter_arc(
&self,
arc1: &Arc<W>,
arc2: &Arc<W>,
_fs: &Self::FilterState,
) -> Option<(Arc<W>, Self::FilterState)> {
use crate::fst::NO_LABEL;
if arc1.olabel == arc2.ilabel || arc1.olabel == NO_LABEL || arc2.ilabel == NO_LABEL {
let composed_ilabel = if arc1.olabel == NO_LABEL && arc2.ilabel == NO_LABEL {
NO_LABEL
} else {
arc1.ilabel
};
let composed_olabel = if arc1.olabel == NO_LABEL && arc2.ilabel == NO_LABEL {
NO_LABEL
} else {
arc2.olabel
};
Some((
Arc::new(
composed_ilabel,
composed_olabel,
arc1.weight.times(&arc2.weight),
0, ),
(),
))
} else {
None
}
}
}
fn compute_epsilon_closure_for_compose<W: Semiring, F: Fst<W>>(
fst: &F,
start: StateId,
) -> Result<FxHashMap<StateId, W>> {
let mut closure = FxHashMap::default();
let mut queue = VecDeque::new();
let mut iteration_count: FxHashMap<StateId, usize> = FxHashMap::default();
const MAX_ITERATIONS: usize = 1000;
queue.push_back((start, W::one()));
closure.insert(start, W::one());
iteration_count.insert(start, 0);
while let Some((state, weight)) = queue.pop_front() {
let iter = iteration_count.get(&state).copied().unwrap_or(0);
if iter >= MAX_ITERATIONS {
continue;
}
for arc in fst.arcs(state) {
if arc.is_epsilon() {
let next_weight = weight.times(&arc.weight);
let next_iter = iter + 1;
let should_update = match closure.get(&arc.nextstate) {
Some(existing) => {
let combined = existing.plus(&next_weight);
if combined != *existing {
closure.insert(arc.nextstate, combined.clone());
true
} else {
false
}
}
None => {
closure.insert(arc.nextstate, next_weight.clone());
true
}
};
if should_update {
if next_iter < MAX_ITERATIONS {
iteration_count.insert(arc.nextstate, next_iter);
queue.push_back((arc.nextstate, closure[&arc.nextstate].clone()));
}
}
}
}
}
Ok(closure)
}
pub fn compose<W, F1, F2, M, CF>(fst1: &F1, fst2: &F2, filter: CF) -> Result<M>
where
W: Semiring,
F1: Fst<W>,
F2: Fst<W>,
M: MutableFst<W> + Default,
CF: ComposeFilter<W>,
{
let start1 = fst1
.start()
.ok_or_else(|| Error::Algorithm("First FST has no start state".into()))?;
let start2 = fst2
.start()
.ok_or_else(|| Error::Algorithm("Second FST has no start state".into()))?;
let mut result = M::default();
let mut state_map = FxHashMap::default();
let mut queue = Vec::new();
let mut processed_via_closure = FxHashSet::default();
let start_state = result.add_state();
result.set_start(start_state);
state_map.insert((start1, start2, CF::start()), start_state);
queue.push((start1, start2, CF::start(), start_state));
while let Some((s1, s2, fs, current)) = queue.pop() {
if processed_via_closure.contains(&(s1, s2)) {
continue;
}
let closure1 = compute_epsilon_closure_for_compose(fst1, s1)?;
let closure2 = compute_epsilon_closure_for_compose(fst2, s2)?;
for (cs1, w1_eps) in &closure1 {
for (cs2, w2_eps) in &closure2 {
if let (Some(w1), Some(w2)) = (fst1.final_weight(*cs1), fst2.final_weight(*cs2)) {
let total_weight = w1_eps.times(w1).times(w2_eps).times(w2);
if let Some(existing) = result.final_weight(current) {
result.set_final(current, existing.plus(&total_weight));
} else {
result.set_final(current, total_weight);
}
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct ArcMapKey<FS> {
ilabel: u32,
olabel: u32,
next_state1: StateId,
next_state2: StateId,
filter_state: FS,
}
let mut arc_map: FxHashMap<ArcMapKey<CF::FilterState>, W> = FxHashMap::default();
let mut created_arcs_from = FxHashSet::default();
let mut created_epsilon_from_current = false;
for (cs1, w1_eps) in &closure1 {
for (cs2, w2_eps) in &closure2 {
let mut created_from_this_pair = false;
for arc1 in fst1.arcs(*cs1) {
for arc2 in fst2.arcs(*cs2) {
if let Some((arc, next_fs)) = filter.filter_arc(&arc1, &arc2, &fs) {
created_from_this_pair = true;
if arc.is_epsilon() && (*cs1, *cs2) == (s1, s2) {
created_epsilon_from_current = true;
}
let total_weight = w1_eps.times(&arc.weight).times(w2_eps);
let arc_key = ArcMapKey {
ilabel: arc.ilabel,
olabel: arc.olabel,
next_state1: arc1.nextstate,
next_state2: arc2.nextstate,
filter_state: next_fs.clone(),
};
match arc_map.get_mut(&arc_key) {
Some(existing_weight) => {
*existing_weight = existing_weight.plus(&total_weight);
}
None => {
arc_map.insert(arc_key, total_weight);
}
}
}
}
}
if created_from_this_pair && (*cs1, *cs2) != (s1, s2) {
created_arcs_from.insert((*cs1, *cs2));
processed_via_closure.insert((*cs1, *cs2));
}
}
}
for (arc_key, weight) in arc_map {
let next_key = (
arc_key.next_state1,
arc_key.next_state2,
arc_key.filter_state,
);
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key.clone(), state);
queue.push((next_key.0, next_key.1, next_key.2, state));
state
}
};
result.add_arc(
current,
Arc::new(arc_key.ilabel, arc_key.olabel, weight, next_state),
);
}
processed_via_closure.insert((s1, s2));
for arc1 in fst1.arcs(s1) {
if arc1.is_epsilon_output() && !arc1.is_epsilon_input() {
let next_key = (arc1.nextstate, s2, CF::start());
if !processed_via_closure.contains(&(arc1.nextstate, s2))
&& !created_arcs_from.contains(&(arc1.nextstate, s2))
{
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key.clone(), state);
queue.push((arc1.nextstate, s2, CF::start(), state));
state
}
};
let composed_arc = Arc::new(
arc1.ilabel,
0, arc1.weight.clone(),
next_state,
);
result.add_arc(current, composed_arc);
}
}
}
for arc2 in fst2.arcs(s2) {
if arc2.is_epsilon_input() && !arc2.is_epsilon_output() {
let next_key = (s1, arc2.nextstate, CF::start());
if !processed_via_closure.contains(&(s1, arc2.nextstate))
&& !created_arcs_from.contains(&(s1, arc2.nextstate))
{
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key.clone(), state);
queue.push((s1, arc2.nextstate, CF::start(), state));
state
}
};
let composed_arc = Arc::new(
0, arc2.olabel,
arc2.weight.clone(),
next_state,
);
result.add_arc(current, composed_arc);
}
}
}
for arc1 in fst1.arcs(s1) {
if arc1.is_epsilon() {
let next_key = (arc1.nextstate, s2, CF::start());
if !created_epsilon_from_current
&& !processed_via_closure.contains(&(arc1.nextstate, s2))
&& !created_arcs_from.contains(&(arc1.nextstate, s2))
{
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key.clone(), state);
queue.push((arc1.nextstate, s2, CF::start(), state));
state
}
};
let composed_arc = Arc::epsilon(arc1.weight.clone(), next_state);
result.add_arc(current, composed_arc);
}
}
}
for arc2 in fst2.arcs(s2) {
if arc2.is_epsilon() {
let next_key = (s1, arc2.nextstate, CF::start());
if !created_epsilon_from_current
&& !processed_via_closure.contains(&(s1, arc2.nextstate))
&& !created_arcs_from.contains(&(s1, arc2.nextstate))
{
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key.clone(), state);
queue.push((s1, arc2.nextstate, CF::start(), state));
state
}
};
let composed_arc = Arc::epsilon(arc2.weight.clone(), next_state);
result.add_arc(current, composed_arc);
}
}
}
}
Ok(result)
}
pub fn compose_default<W, F1, F2, M>(fst1: &F1, fst2: &F2) -> Result<M>
where
W: Semiring,
F1: Fst<W>,
F2: Fst<W>,
M: MutableFst<W> + Default,
{
compose(fst1, fst2, DefaultComposeFilter)
}
pub fn compose_sorted<W, F1, F2, M>(fst1: &F1, fst2: &F2) -> Result<M>
where
W: Semiring,
F1: Fst<W>,
F2: Fst<W>,
M: MutableFst<W> + Default,
{
let start1 = fst1
.start()
.ok_or_else(|| Error::Algorithm("First FST has no start state".into()))?;
let start2 = fst2
.start()
.ok_or_else(|| Error::Algorithm("Second FST has no start state".into()))?;
let mut result = M::default();
let mut state_map: FxHashMap<(StateId, StateId), StateId> = FxHashMap::default();
let mut queue = Vec::new();
let mut sorted_arcs1: FxHashMap<StateId, Vec<Arc<W>>> = FxHashMap::default();
let mut sorted_arcs2: FxHashMap<StateId, Vec<Arc<W>>> = FxHashMap::default();
let start_state = result.add_state();
result.set_start(start_state);
state_map.insert((start1, start2), start_state);
queue.push((start1, start2, start_state));
while let Some((s1, s2, current)) = queue.pop() {
if let (Some(w1), Some(w2)) = (fst1.final_weight(s1), fst2.final_weight(s2)) {
let final_weight = w1.times(w2);
if let Some(existing) = result.final_weight(current) {
result.set_final(current, existing.plus(&final_weight));
} else {
result.set_final(current, final_weight);
}
}
let arcs1 = sorted_arcs1.entry(s1).or_insert_with(|| {
let mut arcs: Vec<Arc<W>> = fst1.arcs(s1).collect();
arcs.sort_by_key(|a| a.olabel);
arcs
});
let arcs2 = sorted_arcs2.entry(s2).or_insert_with(|| {
let mut arcs: Vec<Arc<W>> = fst2.arcs(s2).collect();
arcs.sort_by_key(|a| a.ilabel);
arcs
});
let eps1: Vec<_> = arcs1.iter().filter(|a| a.olabel == 0).collect();
let eps2: Vec<_> = arcs2.iter().filter(|a| a.ilabel == 0).collect();
let non_eps1: Vec<_> = arcs1.iter().filter(|a| a.olabel != 0).collect();
let non_eps2: Vec<_> = arcs2.iter().filter(|a| a.ilabel != 0).collect();
let mut i = 0;
let mut j = 0;
while i < non_eps1.len() && j < non_eps2.len() {
let arc1 = non_eps1[i];
let arc2 = non_eps2[j];
match arc1.olabel.cmp(&arc2.ilabel) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
let label = arc1.olabel;
let mut i_end = i;
while i_end < non_eps1.len() && non_eps1[i_end].olabel == label {
i_end += 1;
}
let mut j_end = j;
while j_end < non_eps2.len() && non_eps2[j_end].ilabel == label {
j_end += 1;
}
for a1 in non_eps1.iter().take(i_end).skip(i) {
for a2 in non_eps2.iter().take(j_end).skip(j) {
let next_key = (a1.nextstate, a2.nextstate);
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key, state);
queue.push((next_key.0, next_key.1, state));
state
}
};
let combined_weight = a1.weight.times(&a2.weight);
result.add_arc(
current,
Arc::new(a1.ilabel, a2.olabel, combined_weight, next_state),
);
}
}
i = i_end;
j = j_end;
}
}
}
for arc1 in &eps1 {
let next_key = (arc1.nextstate, s2);
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key, state);
queue.push((next_key.0, next_key.1, state));
state
}
};
result.add_arc(
current,
Arc::new(arc1.ilabel, 0, arc1.weight.clone(), next_state),
);
}
for arc2 in &eps2 {
let next_key = (s1, arc2.nextstate);
let next_state = match state_map.get(&next_key) {
Some(&state) => state,
None => {
let state = result.add_state();
state_map.insert(next_key, state);
queue.push((next_key.0, next_key.1, state));
state
}
};
result.add_arc(
current,
Arc::new(0, arc2.olabel, arc2.weight.clone(), next_state),
);
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use num_traits::One;
#[test]
fn test_basic_composition() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
let mut found_path = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 3 {
found_path = true;
assert_eq!(*arc.weight.value(), 3.0);
}
}
}
assert!(found_path);
}
#[test]
fn test_composition_no_match() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(3, 4, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
let mut has_final = false;
for state in composed.states() {
if composed.is_final(state) {
has_final = true;
break;
}
}
if has_final {
assert_eq!(composed.num_arcs_total(), 0);
}
}
#[test]
fn test_composition_epsilon() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
let s2 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s2, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.5), s1));
fst1.add_arc(s1, Arc::new(1, 2, TropicalWeight::new(1.0), s2));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
let mut found_path = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 3 {
found_path = true;
assert_eq!(*arc.weight.value(), 3.5);
}
}
}
assert!(found_path, "Should find path from input 1 to output 3");
}
#[test]
fn test_composition_epsilon_input_fst2() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
let t2 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t2, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(0, 0, TropicalWeight::new(0.5), t1)); fst2.add_arc(t1, Arc::new(2, 3, TropicalWeight::new(2.0), t2));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
let mut found_path = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 3 {
found_path = true;
assert_eq!(*arc.weight.value(), 3.5);
}
}
}
assert!(found_path, "Should find path from input 1 to output 3");
}
#[test]
fn test_composition_both_epsilon() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.5), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::epsilon(TropicalWeight::new(0.3), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
let mut found_epsilon = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.is_epsilon() {
found_epsilon = true;
assert_eq!(*arc.weight.value(), 0.8);
}
}
}
assert!(found_epsilon, "Should find epsilon arc in composition");
}
#[test]
fn test_composition_epsilon_output_advances_fst1() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
let s2 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s2, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 0, TropicalWeight::new(0.5), s1));
fst1.add_arc(s1, Arc::new(2, 3, TropicalWeight::new(1.0), s2));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(3, 4, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
let mut found_epsilon_arc = false;
let mut found_final_arc = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 0 {
found_epsilon_arc = true;
assert_eq!(*arc.weight.value(), 0.5);
}
if arc.ilabel == 2 && arc.olabel == 4 {
found_final_arc = true;
assert_eq!(*arc.weight.value(), 3.0);
}
}
}
assert!(
found_epsilon_arc || found_final_arc,
"Should handle epsilon output correctly"
);
}
#[test]
fn test_composition_with_epsilon_cycle_fst1() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
let s2 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s2, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.5), s1));
fst1.add_arc(s1, Arc::epsilon(TropicalWeight::new(0.3), s0));
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s2));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
let mut found_path = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 3 {
found_path = true;
break;
}
}
if found_path {
break;
}
}
assert!(found_path, "Should find path from input 1 to output 3");
}
#[test]
fn test_composition_with_epsilon_cycle_fst2() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
let t2 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t2, TropicalWeight::one());
fst2.add_arc(t0, Arc::epsilon(TropicalWeight::new(0.5), t1));
fst2.add_arc(t1, Arc::epsilon(TropicalWeight::new(0.3), t0));
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t2));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
let mut found_path = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 3 {
found_path = true;
break;
}
}
if found_path {
break;
}
}
assert!(found_path, "Should find path from input 1 to output 3");
}
#[test]
fn test_composition_with_epsilon_cycles_both() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.2), s0));
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::epsilon(TropicalWeight::new(0.3), t0));
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
}
#[test]
fn test_composition_epsilon_cycle_termination() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.1), s0));
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(1.0), t1));
let composed: VectorFst<TropicalWeight> = compose_default(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
}
#[test]
fn test_compose_sorted_basic() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
fst1.add_arc(s0, Arc::new(1, 2, TropicalWeight::new(1.0), s1));
fst1.add_arc(s0, Arc::new(2, 3, TropicalWeight::new(1.5), s1));
fst1.add_arc(s0, Arc::new(3, 4, TropicalWeight::new(2.0), s1));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 5, TropicalWeight::new(0.5), t1));
fst2.add_arc(t0, Arc::new(4, 6, TropicalWeight::new(0.3), t1));
let composed: VectorFst<TropicalWeight> = compose_sorted(&fst1, &fst2).unwrap();
let mut found_path1 = false;
let mut found_path2 = false;
for state in composed.states() {
for arc in composed.arcs(state) {
if arc.ilabel == 1 && arc.olabel == 5 {
found_path1 = true;
assert_eq!(*arc.weight.value(), 1.5);
}
if arc.ilabel == 3 && arc.olabel == 6 {
found_path2 = true;
assert_eq!(*arc.weight.value(), 2.3);
}
}
}
assert!(found_path1, "Should find path 1->5");
assert!(found_path2, "Should find path 3->6");
}
#[test]
fn test_compose_sorted_many_arcs() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s1, TropicalWeight::one());
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
for i in 1..=50 {
fst1.add_arc(
s0,
Arc::new(i, i * 2, TropicalWeight::new(i as f32 * 0.1), s1),
);
}
for i in 1..=50 {
fst2.add_arc(
t0,
Arc::new(i * 2, i * 3, TropicalWeight::new(i as f32 * 0.05), t1),
);
}
let composed: VectorFst<TropicalWeight> = compose_sorted(&fst1, &fst2).unwrap();
let mut arc_count = 0;
for state in composed.states() {
arc_count += composed.num_arcs(state);
}
assert_eq!(arc_count, 50, "Should have 50 composed arcs");
}
#[test]
fn test_compose_sorted_with_epsilon() {
let mut fst1 = VectorFst::<TropicalWeight>::new();
let s0 = fst1.add_state();
let s1 = fst1.add_state();
let s2 = fst1.add_state();
fst1.set_start(s0);
fst1.set_final(s2, TropicalWeight::one());
fst1.add_arc(s0, Arc::epsilon(TropicalWeight::new(0.5), s1)); fst1.add_arc(s1, Arc::new(1, 2, TropicalWeight::new(1.0), s2));
let mut fst2 = VectorFst::<TropicalWeight>::new();
let t0 = fst2.add_state();
let t1 = fst2.add_state();
fst2.set_start(t0);
fst2.set_final(t1, TropicalWeight::one());
fst2.add_arc(t0, Arc::new(2, 3, TropicalWeight::new(2.0), t1));
let composed: VectorFst<TropicalWeight> = compose_sorted(&fst1, &fst2).unwrap();
assert!(composed.start().is_some());
assert!(composed.num_states() > 0);
}
}