use crate::algorithms::accumulator::DefaultAccumulator;
use crate::algorithms::arcsort::{ILabelCompare, OLabelCompare, arc_sort};
use crate::algorithms::compose_filter::ComposeFilter;
use crate::algorithms::compose_filter::SequenceComposeFilter;
use crate::algorithms::label_reachable::{LabelReachable, LabelReachableData};
use crate::algorithms::lookahead_filter::LookAheadComposeFilter;
use crate::algorithms::lookahead_matcher::{
DEFAULT_LABEL_LOOKAHEAD_FLAGS, LabelLookAheadMatcher, OUTPUT_LOOKAHEAD_MATCHER,
TrivialLookAheadMatcher,
};
use crate::arc::{Arc, ArcLabel};
use crate::data_structures::bi_table::BiTableId;
use crate::data_structures::bit_set::GrowableBitSet;
use crate::data_structures::state_table::{
ComposeStateTable, DefaultComposeStateTuple, GenericComposeStateTable,
};
use crate::error::OpenFstError;
use crate::fst::ExpandedFst;
use crate::fst::MatchType;
use crate::fst::{Fst, MutableFst};
use crate::fsts::vector_fst::VectorFst;
use crate::matcher::SortedMatcher;
use crate::matcher::{Matcher, REQUIRE_PRIORITY};
use crate::properties::{K_FST_PROPERTIES, K_NO_I_EPSILONS, compose_properties};
use crate::properties::{K_I_LABEL_SORTED, K_O_LABEL_SORTED};
use crate::weight::Weight;
fn match_types<'f, A, M1, M2>(matcher1: &M1, matcher2: &M2) -> Result<MatchType, OpenFstError>
where
A: Arc,
M1: Matcher<'f, A>,
M2: Matcher<'f, A>,
{
let can_output =
matcher1.match_type() == MatchType::Output || matcher1.match_type() == MatchType::Both;
let can_input =
matcher2.match_type() == MatchType::Input || matcher2.match_type() == MatchType::Both;
match (can_output, can_input) {
(true, true) => Ok(MatchType::Both),
(true, false) => Ok(MatchType::Output),
(false, true) => Ok(MatchType::Input),
(false, false) => Err(OpenFstError::InvalidOperation(
"Compose: neither matcher can match the side composition needs".into(),
)),
}
}
pub fn compose_with<'f, A, F1, F2, FO, Filter>(
fst1: &'f F1,
fst2: &'f F2,
ofst: &mut FO,
filter: &mut Filter,
) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
F1: Fst<A>,
F2: Fst<A>,
FO: MutableFst<A>,
Filter: ComposeFilter<Arc = A>,
Filter::Matcher1: Matcher<'f, A>,
Filter::Matcher2: Matcher<'f, A>,
{
ofst.delete_all_states();
ofst.set_input_symbols(fst1.input_symbols());
ofst.set_output_symbols(fst2.output_symbols());
let match_type = match_types(filter.matcher1(), filter.matcher2())?;
let props1 = fst1.properties(K_FST_PROPERTIES, false);
let props2 = fst2.properties(K_FST_PROPERTIES, false);
let props = filter.properties(compose_properties(props1, props2));
let (Some(start1), Some(start2)) = (fst1.start(), fst2.start()) else {
ofst.set_properties(props, K_FST_PROPERTIES);
return Ok(());
};
let mut states: GenericComposeStateTable<A, Filter::FilterState> =
GenericComposeStateTable::new(fst1, fst2);
let start = states.find_state(&DefaultComposeStateTuple::new(
start1,
start2,
filter.start(),
));
ofst.add_state();
ofst.set_start(start);
let mut pending: Vec<A::StateId> = vec![start];
let mut done = GrowableBitSet::new();
done.insert(start.as_usize());
let zero = A::Weight::zero();
let epsilon = A::Label::epsilon();
let no_label = A::Label::no_label();
let mut arcs: Vec<A> = Vec::new();
while let Some(state) = pending.pop() {
let tuple = states.tuple(state).clone();
let (s1, s2) = (tuple.state_id1(), tuple.state_id2());
let mut final1 = fst1.final_weight(s1);
let mut final2 = fst2.final_weight(s2);
if final1 != zero && final2 != zero {
filter.set_state(s1, s2, tuple.get_filter_state());
filter.filter_final(&mut final1, &mut final2);
let weight = final1.times(&final2);
if weight != zero {
ofst.set_final(state, weight);
}
}
filter.set_state(s1, s2, tuple.get_filter_state());
let match_input = match match_type {
MatchType::Input => true,
MatchType::Output => false,
_ => {
let priority1 = filter.matcher1_mut().priority(s1);
let priority2 = filter.matcher2_mut().priority(s2);
if priority1 == REQUIRE_PRIORITY && priority2 == REQUIRE_PRIORITY {
return Err(OpenFstError::InvalidOperation(
"Compose: both sides require the match to be made on them".into(),
));
}
if priority1 == REQUIRE_PRIORITY {
false
} else if priority2 == REQUIRE_PRIORITY {
true
} else {
priority1 <= priority2
}
}
};
arcs.clear();
if match_input {
filter.matcher2_mut().set_state(s2);
let loop_arc = A::new(epsilon, no_label, A::Weight::one(), s1);
let walked: Vec<A> = std::iter::once(loop_arc).chain(fst1.arcs(s1)).collect();
for arc in walked {
match_arc(filter, &mut states, &mut arcs, &arc, true);
}
} else {
filter.matcher1_mut().set_state(s1);
let loop_arc = A::new(no_label, epsilon, A::Weight::one(), s2);
let walked: Vec<A> = std::iter::once(loop_arc).chain(fst2.arcs(s2)).collect();
for arc in walked {
match_arc(filter, &mut states, &mut arcs, &arc, false);
}
}
while ofst.num_states() < states.size() {
ofst.add_state();
}
for arc in arcs.drain(..) {
let next = arc.nextstate();
if done.insert(next.as_usize()) {
pending.push(next);
}
ofst.add_arc(state, arc);
}
}
ofst.set_properties(props, K_FST_PROPERTIES);
Ok(())
}
pub fn sorted_copy<A, F>(fst: &F, by_output: bool) -> VectorFst<A>
where
A: Arc,
F: Fst<A> + ExpandedFst<A>,
{
let mut copy: VectorFst<A> = VectorFst::new();
copy.add_states(fst.num_states());
copy.set_input_symbols(fst.input_symbols());
copy.set_output_symbols(fst.output_symbols());
if let Some(start) = fst.start() {
copy.set_start(start);
}
for state in fst.states() {
copy.set_final(state, fst.final_weight(state));
for arc in fst.arcs(state) {
copy.add_arc(state, arc);
}
}
copy.set_properties(fst.properties(K_FST_PROPERTIES, false), K_FST_PROPERTIES);
let wanted = if by_output {
K_O_LABEL_SORTED
} else {
K_I_LABEL_SORTED
};
if copy.properties(wanted, true) & wanted == 0 {
if by_output {
arc_sort(&mut copy, &OLabelCompare);
} else {
arc_sort(&mut copy, &ILabelCompare);
}
}
copy
}
pub fn compose<A, F1, F2, FO>(fst1: &F1, fst2: &F2, ofst: &mut FO) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
F1: Fst<A> + ExpandedFst<A>,
F2: Fst<A> + ExpandedFst<A>,
FO: MutableFst<A> + ExpandedFst<A>,
{
compose_options(fst1, fst2, ofst, &ComposeOptions::default())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComposeOptions {
pub connect: bool,
}
impl Default for ComposeOptions {
fn default() -> Self {
Self { connect: true }
}
}
pub fn compose_options<A, F1, F2, FO>(
fst1: &F1,
fst2: &F2,
ofst: &mut FO,
opts: &ComposeOptions,
) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
F1: Fst<A> + ExpandedFst<A>,
F2: Fst<A> + ExpandedFst<A>,
FO: MutableFst<A> + ExpandedFst<A>,
{
let left = sorted_copy(fst1, true);
let right = sorted_copy(fst2, false);
let matcher1 = SortedMatcher::new(&left, MatchType::Output)?;
let matcher2 = SortedMatcher::new(&right, MatchType::Input)?;
let mut filter = SequenceComposeFilter::new(&left, matcher1, matcher2);
compose_with(&left, &right, ofst, &mut filter)?;
if opts.connect {
crate::algorithms::connect::connect(ofst);
}
Ok(())
}
pub fn compose_lookahead<A, F1, F2, FO>(
fst1: &F1,
fst2: &F2,
ofst: &mut FO,
) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
F1: Fst<A> + ExpandedFst<A>,
F2: Fst<A> + ExpandedFst<A>,
FO: MutableFst<A> + ExpandedFst<A>,
{
let required = K_NO_I_EPSILONS;
if fst2.properties(required, true) & required != required {
return Err(OpenFstError::InvalidOperation(
"ComposeLookAhead: the 2nd argument has input epsilons, which the look-ahead index \
cannot see past; remove them first"
.into(),
));
}
let left = sorted_copy(fst1, true);
let right = sorted_copy(fst2, false);
compose_lookahead_sorted(&left, &right, ofst)
}
pub fn lookahead_index<A, F>(fst1: &F) -> Result<std::sync::Arc<LabelReachableData>, OpenFstError>
where
A: Arc,
F: Fst<A> + ExpandedFst<A>,
{
let reachable =
LabelReachable::<A, DefaultAccumulator>::with_accumulator(fst1, false, DefaultAccumulator)?;
Ok(std::sync::Arc::clone(reachable.data()))
}
pub fn compose_lookahead_sorted<A, FO>(
left: &VectorFst<A>,
right: &VectorFst<A>,
ofst: &mut FO,
) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
FO: MutableFst<A> + ExpandedFst<A>,
{
compose_lookahead_indexed(left, &lookahead_index(left)?, right, ofst)
}
pub fn compose_lookahead_indexed<A, FO>(
left: &VectorFst<A>,
index: &std::sync::Arc<LabelReachableData>,
right: &VectorFst<A>,
ofst: &mut FO,
) -> Result<(), OpenFstError>
where
A: Arc,
A::StateId: BiTableId,
FO: MutableFst<A> + ExpandedFst<A>,
{
let matcher1 = LabelLookAheadMatcher::from_data(
std::sync::Arc::clone(index),
SortedMatcher::new(left, MatchType::Output)?,
DEFAULT_LABEL_LOOKAHEAD_FLAGS | OUTPUT_LOOKAHEAD_MATCHER,
DefaultAccumulator,
);
let matcher2 = TrivialLookAheadMatcher::new(SortedMatcher::new(right, MatchType::Input)?);
let inner = SequenceComposeFilter::new(left, matcher1, matcher2);
let mut filter = LookAheadComposeFilter::new(inner, right)?;
compose_with(left, right, ofst, &mut filter)?;
crate::algorithms::connect::connect(ofst);
Ok(())
}
fn match_arc<'f, A, Filter>(
filter: &mut Filter,
states: &mut GenericComposeStateTable<A, Filter::FilterState>,
arcs: &mut Vec<A>,
arc: &A,
match_input: bool,
) where
A: Arc,
A::StateId: BiTableId,
Filter: ComposeFilter<Arc = A>,
Filter::Matcher1: Matcher<'f, A>,
Filter::Matcher2: Matcher<'f, A>,
{
let label = if match_input {
arc.olabel()
} else {
arc.ilabel()
};
let found: Vec<A> = if match_input {
let matcher = filter.matcher2_mut();
if !matcher.find(label) {
return;
}
let mut out = Vec::new();
while !matcher.done() {
out.push(matcher.value());
matcher.next();
}
out
} else {
let matcher = filter.matcher1_mut();
if !matcher.find(label) {
return;
}
let mut out = Vec::new();
while !matcher.done() {
out.push(matcher.value());
matcher.next();
}
out
};
for other in found {
let (mut arc1, mut arc2) = if match_input {
(arc.clone(), other)
} else {
(other, arc.clone())
};
let Some(fs) = filter.filter_arc(&mut arc1, &mut arc2) else {
continue;
};
let next = states.find_state(&DefaultComposeStateTuple::new(
arc1.nextstate(),
arc2.nextstate(),
fs,
));
arcs.push(A::new(
arc1.ilabel(),
arc2.olabel(),
arc1.weight().times(arc2.weight()),
next,
));
}
}
#[cfg(test)]
mod tests {
fn epsilon_acceptor(
rng: &mut crate::algorithms::test_support::Rng,
states: usize,
) -> StdVectorFst {
let mut fst = StdVectorFst::new();
for _ in 0..states {
fst.add_state();
}
fst.set_start(0);
for s in 0..states {
for _ in 0..3 {
let draw = rng.below(4);
let label = if draw == 0 { 0 } else { draw as i32 };
let room = states - s - 1;
if room == 0 {
continue;
}
let next = s + 1 + rng.below(room);
fst.add_arc(
s as i32,
StdArc::new(label, label, TropicalWeight::one(), next as i32),
);
}
if s % 3 == 0 {
fst.set_final(s as i32, TropicalWeight::one());
}
}
fst.properties(K_FST_PROPERTIES, true);
fst
}
#[test]
fn looking_ahead_composes_to_the_same_fst() {
use crate::algorithms::rmepsilon::rm_epsilon;
use crate::algorithms::test_support::{Rng, string_weights, visible_paths};
let mut rng = Rng::new(0x_C0FFEE);
for round in 0..40 {
let first = epsilon_acceptor(&mut rng, 60);
let mut second = epsilon_acceptor(&mut rng, 60);
rm_epsilon(&mut second, true).expect("epsilons removed");
let mut plain = StdVectorFst::new();
compose(&first, &second, &mut plain).expect("a composition");
let mut ahead = StdVectorFst::new();
compose_lookahead(&first, &second, &mut ahead).expect("a composition");
assert_eq!(
string_weights(visible_paths(&plain, 12)),
string_weights(visible_paths(&ahead, 12)),
"round {round}"
);
}
}
#[test]
fn a_second_argument_with_input_epsilons_is_refused() {
let mut first = StdVectorFst::new();
for _ in 0..2 {
first.add_state();
}
first.set_start(0);
first.set_final(1, TropicalWeight::one());
first.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 1));
first.properties(K_FST_PROPERTIES, true);
let mut second = StdVectorFst::new();
for _ in 0..3 {
second.add_state();
}
second.set_start(0);
second.set_final(2, TropicalWeight::one());
second.add_arc(0, StdArc::new(0, 0, TropicalWeight::one(), 1));
second.add_arc(1, StdArc::new(1, 1, TropicalWeight::one(), 2));
second.properties(K_FST_PROPERTIES, true);
let mut out = StdVectorFst::new();
let Err(err) = compose_lookahead(&first, &second, &mut out) else {
panic!("an input epsilon on the second side is not something to look past")
};
assert!(format!("{err}").contains("input epsilons"), "{err}");
let mut out = StdVectorFst::new();
compose(&first, &second, &mut out).expect("a composition");
assert!(out.num_states() > 0);
}
#[test]
fn the_pairs_that_lead_nowhere_are_not_in_the_answer() {
use crate::algorithms::connect::connect;
let mut fst1 = StdVectorFst::new();
for _ in 0..3 {
fst1.add_state();
}
fst1.set_start(0);
fst1.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 1));
fst1.add_arc(1, StdArc::new(2, 2, TropicalWeight::one(), 2));
fst1.set_final(2, TropicalWeight::one());
fst1.properties(K_FST_PROPERTIES, true);
let mut fst2 = StdVectorFst::new();
for _ in 0..3 {
fst2.add_state();
}
fst2.set_start(0);
fst2.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 1));
fst2.add_arc(1, StdArc::new(3, 3, TropicalWeight::one(), 2));
fst2.set_final(2, TropicalWeight::one());
fst2.properties(K_FST_PROPERTIES, true);
let mut connected = StdVectorFst::new();
compose(&fst1, &fst2, &mut connected).unwrap();
assert_eq!(
connected.num_states(),
0,
"nothing composes, so there is nothing to keep"
);
let mut whole = StdVectorFst::new();
compose_options(&fst1, &fst2, &mut whole, &ComposeOptions { connect: false }).unwrap();
assert!(
whole.num_states() > 0,
"without connecting, the pairs it had to build to find out are still there"
);
connect(&mut whole);
assert_eq!(whole.num_states(), 0, "and connecting is what removes them");
}
use super::*;
use crate::algorithms::test_support::{Rng, random_acyclic_fst, string_weights, visible_paths};
use crate::arc::StdArc;
use crate::fsts::vector_fst::StdVectorFst;
use crate::properties::K_FST_PROPERTIES;
use crate::weights::float_weight::TropicalWeight;
fn chain(arcs: &[(i32, i32, f32)]) -> StdVectorFst {
let mut fst = StdVectorFst::new();
let mut state = fst.add_state();
fst.set_start(state);
for (ilabel, olabel, weight) in arcs {
let next = fst.add_state();
fst.add_arc(
state,
StdArc::new(*ilabel, *olabel, TropicalWeight(*weight), next),
);
state = next;
}
fst.set_final(state, TropicalWeight::one());
fst.properties(K_FST_PROPERTIES, true);
fst
}
fn composed(fst1: &StdVectorFst, fst2: &StdVectorFst) -> StdVectorFst {
let mut out = StdVectorFst::new();
compose(fst1, fst2, &mut out).unwrap();
out
}
fn transduction(fst: &StdVectorFst) -> Vec<(Vec<i32>, Vec<i32>, String)> {
string_weights(visible_paths(fst, 16))
}
#[test]
fn composition_runs_one_output_into_the_others_input() {
let first = chain(&[(1, 2, 1.0)]);
let second = chain(&[(2, 3, 2.0)]);
assert_eq!(
transduction(&composed(&first, &second)),
vec![(vec![1], vec![3], "3.0000".to_string())]
);
}
#[test]
fn nothing_comes_out_when_the_labels_do_not_meet() {
let first = chain(&[(1, 2, 0.0)]);
let second = chain(&[(9, 3, 0.0)]);
assert!(transduction(&composed(&first, &second)).is_empty());
}
#[test]
fn composing_with_the_identity_changes_nothing() {
let first = chain(&[(1, 5, 1.0), (2, 6, 2.0)]);
let mut identity = StdVectorFst::new();
let state = identity.add_state();
identity.set_start(state);
identity.set_final(state, TropicalWeight::one());
for label in [5, 6] {
identity.add_arc(
state,
StdArc::new(label, label, TropicalWeight::one(), state),
);
}
identity.properties(K_FST_PROPERTIES, true);
assert_eq!(
transduction(&composed(&first, &identity)),
transduction(&first)
);
}
#[test]
fn composition_is_associative() {
let a = chain(&[(1, 2, 1.0), (3, 4, 2.0)]);
let b = chain(&[(2, 5, 4.0), (4, 6, 8.0)]);
let c = chain(&[(5, 7, 16.0), (6, 8, 32.0)]);
let left = composed(&composed(&a, &b), &c);
let right = composed(&a, &composed(&b, &c));
assert_eq!(transduction(&left), transduction(&right));
assert_eq!(
transduction(&left),
vec![(vec![1, 3], vec![7, 8], "63.0000".to_string())]
);
}
#[test]
fn an_epsilon_on_the_meeting_side_is_followed_alone() {
let first = chain(&[(1, 0, 1.0), (2, 3, 2.0)]);
let second = chain(&[(3, 4, 4.0)]);
assert_eq!(
transduction(&composed(&first, &second)),
vec![(vec![1, 2], vec![4], "7.0000".to_string())]
);
}
#[test]
fn epsilons_on_both_sides_do_not_double_a_path() {
let first = chain(&[(1, 0, 0.0), (2, 3, 0.0)]);
let second = chain(&[(0, 7, 0.0), (3, 8, 0.0)]);
let out = composed(&first, &second);
let paths = transduction(&out);
assert_eq!(paths.len(), 1, "{paths:?}");
assert_eq!(paths[0].0, vec![1, 2]);
}
#[test]
fn composing_with_nothing_gives_nothing() {
let first = chain(&[(1, 2, 0.0)]);
let empty = StdVectorFst::new();
assert_eq!(composed(&first, &empty).num_states(), 0);
assert_eq!(composed(&empty, &first).num_states(), 0);
}
#[test]
fn the_result_is_what_running_one_into_the_other_gives() {
let mut rng = Rng::new(0x0C0F_0FE5_u64);
let mut checked = 0;
for round in 0..200 {
let make = |rng: &mut Rng, shift: i32| {
let mut fst = random_acyclic_fst(rng, 5);
let states: Vec<i32> = fst.states().collect();
for state in states {
fst.mutate_arcs(state, |arc| {
*arc = StdArc::new(
arc.ilabel(),
arc.ilabel() + shift,
*arc.weight(),
arc.nextstate(),
);
});
}
fst.properties(K_FST_PROPERTIES, true);
fst
};
let first = make(&mut rng, 0);
let second = make(&mut rng, 10);
let mut want: std::collections::BTreeMap<(Vec<i32>, Vec<i32>), f32> =
std::collections::BTreeMap::new();
for (i1, o1, w1) in visible_paths(&first, 12) {
for (i2, o2, w2) in visible_paths(&second, 12) {
if o1 != i2 {
continue;
}
let weight = w1.value() + w2.value();
want.entry((i1.clone(), o2))
.and_modify(|at| *at = at.min(weight))
.or_insert(weight);
}
}
let want: Vec<(Vec<i32>, Vec<i32>, String)> = want
.into_iter()
.map(|((i, o), w)| (i, o, format!("{w:.4}")))
.collect();
if !want.is_empty() {
checked += 1;
}
assert_eq!(
transduction(&composed(&first, &second)),
want,
"round {round}"
);
}
assert!(checked > 20, "only {checked} compositions said anything");
}
}