use crate::arc::{Arc, ArcLabel, ArcStateId};
use crate::error::OpenFstError;
use crate::fst::{ExpandedFst, Fst, MutableFst};
use crate::properties::{
K_COPY_PROPERTIES, K_ERROR, K_FST_PROPERTIES, K_INITIAL_ACYCLIC, union_properties,
};
use crate::symbol_table::compat_symbols_rc;
use crate::weight::Weight;
pub fn union<A, F1, F2>(fst1: &mut F1, fst2: &F2) -> Result<(), OpenFstError>
where
A: Arc,
F1: MutableFst<A> + ExpandedFst<A>,
F2: Fst<A>,
{
if !compat_symbols_rc(fst1.input_symbols(), fst2.input_symbols())
|| !compat_symbols_rc(fst1.output_symbols(), fst2.output_symbols())
{
fst1.set_properties(K_ERROR, K_ERROR);
return Err(OpenFstError::SymbolTable(
"Union: the two FSTs' symbol tables do not agree".into(),
));
}
let numstates1 = fst1.num_states();
let initial_acyclic1 = fst1.properties(K_INITIAL_ACYCLIC, false) & K_INITIAL_ACYCLIC != 0;
let props1 = fst1.properties(K_FST_PROPERTIES, false);
let props2 = fst2.properties(K_FST_PROPERTIES, false);
let Some(start2) = fst2.start() else {
if props2 & K_ERROR != 0 {
fst1.set_properties(K_ERROR, K_ERROR);
}
return Ok(());
};
if let Some(numstates2) = fst2.num_states_if_known() {
fst1.reserve_states(numstates1 + numstates2 + usize::from(!initial_acyclic1));
}
for state2 in fst2.states() {
let state1 = fst1.add_state();
fst1.set_final(state1, fst2.final_weight(state2));
for arc in fst2.arcs(state2) {
let shifted = A::StateId::from_usize(arc.nextstate().as_usize() + numstates1);
fst1.add_arc(
state1,
A::new(arc.ilabel(), arc.olabel(), arc.weight().clone(), shifted),
);
}
}
let shifted_start2 = A::StateId::from_usize(start2.as_usize() + numstates1);
let Some(start1) = fst1.start() else {
fst1.set_start(shifted_start2);
fst1.set_properties(props2, K_COPY_PROPERTIES);
return Ok(());
};
let epsilon = A::Label::epsilon();
if initial_acyclic1 {
fst1.add_arc(
start1,
A::new(epsilon, epsilon, A::Weight::one(), shifted_start2),
);
} else {
let nstart = fst1.add_state();
fst1.set_start(nstart);
fst1.add_arc(nstart, A::new(epsilon, epsilon, A::Weight::one(), start1));
fst1.add_arc(
nstart,
A::new(epsilon, epsilon, A::Weight::one(), shifted_start2),
);
}
fst1.set_properties(union_properties(props1, props2, false), K_FST_PROPERTIES);
Ok(())
}
pub fn union_all<A, F1, F2>(fst1: &mut F1, fsts2: &[&F2]) -> Result<(), OpenFstError>
where
A: Arc,
F1: MutableFst<A> + ExpandedFst<A>,
F2: Fst<A>,
{
let total: usize = fsts2.iter().map(|fst| fst.count_states()).sum();
fst1.reserve_states(1 + fst1.num_states() + total);
for fst2 in fsts2 {
union(fst1, *fst2)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AtomicRc;
use crate::algorithms::test_support::{Rng, random_acyclic_fst, visible_paths};
use crate::arc::StdArc;
use crate::fsts::vector_fst::StdVectorFst;
use crate::properties::K_FST_PROPERTIES;
use crate::symbol_table::SymbolTable;
use crate::weights::float_weight::TropicalWeight;
fn chain(labels: &[i32], weight: f32) -> StdVectorFst {
let mut fst = StdVectorFst::new();
let mut state = fst.add_state();
fst.set_start(state);
for label in labels {
let next = fst.add_state();
fst.add_arc(
state,
StdArc::new(*label, *label, TropicalWeight::one(), next),
);
state = next;
}
fst.set_final(state, TropicalWeight(weight));
fst.properties(K_FST_PROPERTIES, true);
fst
}
fn language(fst: &StdVectorFst) -> Vec<(Vec<i32>, String)> {
let mut out: Vec<(Vec<i32>, String)> = visible_paths(fst, 16)
.into_iter()
.map(|(ilabels, _, weight)| (ilabels, format!("{:.4}", weight.value())))
.collect();
out.sort();
out
}
#[test]
fn the_union_accepts_what_either_accepts() {
let mut fst = chain(&[1, 2], 1.0);
union(&mut fst, &chain(&[3], 2.0)).unwrap();
assert_eq!(
language(&fst),
vec![
(vec![1, 2], "1.0000".to_string()),
(vec![3], "2.0000".to_string())
]
);
}
#[test]
fn a_union_with_nothing_is_the_original() {
let before = language(&chain(&[1, 2], 1.0));
let mut fst = chain(&[1, 2], 1.0);
union(&mut fst, &StdVectorFst::new()).unwrap();
assert_eq!(language(&fst), before);
}
#[test]
fn a_union_into_nothing_is_the_other() {
let mut fst = StdVectorFst::new();
let other = chain(&[4, 5], 3.0);
union(&mut fst, &other).unwrap();
assert_eq!(language(&fst), language(&other));
}
#[test]
fn a_cycle_through_the_start_state_forces_a_new_one() {
let mut cyclic = StdVectorFst::new();
for _ in 0..2 {
cyclic.add_state();
}
cyclic.set_start(0);
cyclic.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 1));
cyclic.add_arc(1, StdArc::new(2, 2, TropicalWeight::one(), 0));
cyclic.set_final(1, TropicalWeight::one());
cyclic.properties(K_FST_PROPERTIES, true);
let before = cyclic.start();
let mut fst = cyclic.clone();
union(&mut fst, &chain(&[9], 0.0)).unwrap();
assert_ne!(fst.start(), before, "the start state was replaced");
let strings: Vec<Vec<i32>> = language(&fst).into_iter().map(|(s, _)| s).collect();
assert!(strings.contains(&vec![9]));
assert!(!strings.iter().any(|s| s.len() > 1 && s.contains(&9)));
}
#[test]
fn the_union_is_the_union_of_the_two_languages() {
let mut rng = Rng::new(0x0011_0110_u64);
for round in 0..200 {
let fst1 = random_acyclic_fst(&mut rng, 5);
let fst2 = random_acyclic_fst(&mut rng, 5);
let mut want: Vec<(Vec<i32>, String)> = language(&fst1);
want.extend(language(&fst2));
want.sort();
let mut merged: std::collections::BTreeMap<Vec<i32>, f32> =
std::collections::BTreeMap::new();
for (string, weight) in want {
let weight: f32 = weight.parse().unwrap();
merged
.entry(string)
.and_modify(|best| *best = best.min(weight))
.or_insert(weight);
}
let want: Vec<(Vec<i32>, String)> = merged
.into_iter()
.map(|(s, w)| (s, format!("{w:.4}")))
.collect();
let mut got = fst1.clone();
union(&mut got, &fst2).unwrap();
let mut merged: std::collections::BTreeMap<Vec<i32>, f32> =
std::collections::BTreeMap::new();
for (string, weight) in language(&got) {
let weight: f32 = weight.parse().unwrap();
merged
.entry(string)
.and_modify(|best| *best = best.min(weight))
.or_insert(weight);
}
let got: Vec<(Vec<i32>, String)> = merged
.into_iter()
.map(|(s, w)| (s, format!("{w:.4}")))
.collect();
assert_eq!(got, want, "round {round}");
}
}
#[test]
fn symbol_tables_that_disagree_are_refused() {
let mut ours = SymbolTable::new("ours");
ours.add_symbol("a", 1);
let mut theirs = SymbolTable::new("theirs");
theirs.add_symbol("b", 1);
let mut fst1 = chain(&[1], 0.0);
fst1.set_input_symbols(Some(AtomicRc::new(ours)));
let mut fst2 = chain(&[1], 0.0);
fst2.set_input_symbols(Some(AtomicRc::new(theirs)));
let err = union(&mut fst1, &fst2).unwrap_err();
assert!(format!("{err}").contains("symbol tables"), "{err}");
}
#[test]
fn unioning_several_at_once_is_unioning_each() {
let parts = [chain(&[1], 0.0), chain(&[2], 1.0), chain(&[3], 2.0)];
let refs: Vec<&StdVectorFst> = parts.iter().collect();
let mut all = StdVectorFst::new();
union_all(&mut all, &refs).unwrap();
let mut one_by_one = StdVectorFst::new();
for part in &parts {
union(&mut one_by_one, part).unwrap();
}
assert_eq!(language(&all), language(&one_by_one));
assert_eq!(language(&all).len(), 3);
}
}