use std::cell::RefCell;
use std::rc::Rc;
use crate::algorithms::connect::connect;
use crate::algorithms::determinize::{DefaultCommonDivisor, determinize_fsa_with_distance};
use crate::algorithms::reverse::reverse;
use crate::algorithms::shortest_distance::{
Distance, SHORTEST_DELTA, ShortestDistanceOptions, shortest_distance_with,
};
use crate::arc::{Arc, ArcLabel, ArcStateId};
use crate::arc_filter::AnyArcFilter;
use crate::data_structures::bit_set::GrowableBitSet;
use crate::data_structures::indexed_heap::IndexedHeap;
use crate::error::OpenFstError;
use crate::fst::{ExpandedFst, Fst, MutableFst};
use crate::fsts::vector_fst::VectorFst;
use crate::properties::{K_ACCEPTOR, K_FST_PROPERTIES, shortest_path_properties};
use crate::queue::{AutoQueue, Queue, natural_less_unchecked, state_weight_compare};
use crate::weight::{Divide, PATH, PathWeight, Weight, natural_less};
#[derive(Debug, Clone)]
pub struct ShortestPathOptions<W> {
pub nshortest: usize,
pub delta: f32,
pub first_path: bool,
pub weight_threshold: W,
pub state_threshold: Option<usize>,
}
impl<W: Weight> Default for ShortestPathOptions<W> {
fn default() -> Self {
Self {
nshortest: 1,
delta: SHORTEST_DELTA,
first_path: false,
weight_threshold: W::zero(),
state_threshold: None,
}
}
}
#[derive(Clone, Copy)]
struct Parent<S> {
from: S,
position: u32,
}
const NO_POSITION: u32 = u32::MAX;
impl<S: ArcStateId> Parent<S> {
fn none() -> Self {
Self {
from: S::no_state(),
position: NO_POSITION,
}
}
fn from(&self) -> Option<S> {
(self.from != S::no_state()).then_some(self.from)
}
}
fn single_shortest_path<A, F, Q>(
ifst: &F,
distance: &Distance<A::Weight>,
queue: &mut Q,
parent: &mut Vec<Parent<A::StateId>>,
first_path: bool,
) -> Result<Option<A::StateId>, OpenFstError>
where
A: Arc,
A::Weight: PathWeight,
F: Fst<A>,
Q: Queue<A::StateId>,
{
parent.clear();
distance.borrow_mut().clear();
let Some(source) = ifst.start() else {
return Ok(None);
};
queue.clear();
let mut enqueued = GrowableBitSet::new();
let mut grown = 0usize;
let ensure = |distance: &Distance<A::Weight>,
parent: &mut Vec<Parent<A::StateId>>,
grown: &mut usize,
index: usize| {
if index < *grown {
return;
}
let mut distance = distance.borrow_mut();
while distance.len() <= index {
distance.push(A::Weight::zero());
parent.push(Parent::none());
}
*grown = distance.len();
};
let source_index = source.as_usize();
ensure(distance, parent, &mut grown, source_index);
distance.borrow_mut()[source_index] = A::Weight::one();
enqueued.insert(source_index);
queue.enqueue(source);
let zero = A::Weight::zero();
let mut best_final: Option<A::StateId> = None;
let mut best_distance = zero.clone();
let mut final_seen = false;
while let Some(state) = queue.dequeue() {
let index = state.as_usize();
ensure(distance, parent, &mut grown, index);
enqueued.remove(index);
let here = distance.borrow()[index].clone();
if first_path && final_seen && !natural_less(&here, &best_distance) {
break;
}
let final_here = ifst.final_weight(state);
if final_here != zero {
let through = here.times(&final_here);
let sum = best_distance.plus(&through);
if sum != best_distance {
best_distance = sum;
best_final = Some(state);
}
if !best_distance.is_member() {
return Err(OpenFstError::InvalidOperation(
"ShortestPath: the best distance left the semiring".into(),
));
}
final_seen = true;
}
for (position, arc) in ifst.arcs(state).enumerate() {
let next = arc.nextstate().as_usize();
ensure(distance, parent, &mut grown, next);
let weight = here.times(arc.weight());
let sum = {
let mut distance = distance.borrow_mut();
let current = &distance[next];
let sum = current.plus(&weight);
if sum == *current {
continue;
}
distance[next] = sum.clone();
sum
};
if !sum.is_member() {
return Err(OpenFstError::InvalidOperation(
"ShortestPath: a distance left the semiring".into(),
));
}
parent[next] = Parent {
from: state,
position: position as u32,
};
if enqueued.contains(next) {
queue.update(arc.nextstate());
} else {
queue.enqueue(arc.nextstate());
enqueued.insert(next);
}
}
}
Ok(best_final)
}
fn backtrace<A, F1, F2>(
ifst: &F1,
ofst: &mut F2,
parent: &[Parent<A::StateId>],
best_final: Option<A::StateId>,
) where
A: Arc,
F1: Fst<A>,
F2: MutableFst<A>,
{
ofst.delete_all_states();
ofst.set_input_symbols(ifst.input_symbols());
ofst.set_output_symbols(ifst.output_symbols());
let Some(best_final) = best_final else {
return;
};
let mut here: Option<A::StateId> = None;
let mut previous: Option<A::StateId>;
let mut state = Some(best_final);
let mut came_from: Option<A::StateId> = None;
while let Some(at) = state {
previous = here;
let made = ofst.add_state();
here = Some(made);
match came_from {
None => ofst.set_final(made, ifst.final_weight(best_final)),
Some(from) => {
let position = parent[from.as_usize()].position;
debug_assert_ne!(position, NO_POSITION, "a state with a parent has an arc");
if let Some(arc) = ifst.arcs(at).nth(position as usize) {
ofst.add_arc(
made,
A::new(
arc.ilabel(),
arc.olabel(),
arc.weight().clone(),
previous.expect("the state reached before this one"),
),
);
}
}
}
came_from = Some(at);
state = parent[at.as_usize()].from();
}
if let Some(start) = here {
ofst.set_start(start);
}
let props = shortest_path_properties(ofst.properties(K_FST_PROPERTIES, false), true);
ofst.set_properties(props, K_FST_PROPERTIES);
}
type Pair<S, W> = (Option<S>, W);
#[allow(clippy::too_many_arguments)]
fn n_shortest_path<A, F1, F2>(
rfst: &F1,
ofst: &mut F2,
distance: &[A::Weight],
nshortest: usize,
delta: f32,
weight_threshold: &A::Weight,
state_threshold: Option<usize>,
) -> Result<(), OpenFstError>
where
A: Arc,
A::Weight: PathWeight,
A::Reverse: Arc<Label = A::Label, StateId = A::StateId>,
<<A::Reverse as Arc>::Weight as Weight>::ReverseWeight: Into<A::Weight>,
F1: Fst<A::Reverse> + ExpandedFst<A::Reverse>,
F2: MutableFst<A> + ExpandedFst<A>,
{
ofst.delete_all_states();
ofst.set_input_symbols(rfst.input_symbols());
ofst.set_output_symbols(rfst.output_symbols());
if nshortest == 0 || state_threshold == Some(0) {
return Ok(());
}
let Some(rstart) = rfst.start() else {
return Ok(());
};
let start_index = rstart.as_usize();
if distance.len() <= start_index || distance[start_index] == A::Weight::zero() {
return Ok(());
}
if natural_less(weight_threshold, &A::Weight::one()) {
return Ok(());
}
let pairs: Rc<RefCell<Vec<Pair<A::StateId, A::Weight>>>> = Rc::new(RefCell::new(Vec::new()));
let owned_distance: Vec<A::Weight> = distance.to_vec();
let worse = {
let pairs = Rc::clone(&pairs);
let distance = owned_distance.clone();
move |x: &usize, y: &usize| -> bool {
let pairs = pairs.borrow();
let at = |index: usize| -> A::Weight {
let (state, weight): &Pair<A::StateId, A::Weight> = &pairs[index];
let d = match state {
None => A::Weight::one(),
Some(state) => distance
.get(state.as_usize())
.cloned()
.unwrap_or_else(A::Weight::zero),
};
d.times(weight)
};
let (wx, wy) = (at(*x), at(*y));
let x_complete = pairs[*x].0.is_none();
let y_complete = pairs[*y].0.is_none();
match (x_complete, y_complete) {
(true, false) => natural_less(&wy, &wx) || wx.approx_equal(&wy, delta),
(false, true) => natural_less(&wy, &wx) && !wx.approx_equal(&wy, delta),
_ => natural_less(&wy, &wx),
}
}
};
let mut heap = IndexedHeap::new(move |x: &usize, y: &usize| worse(y, x));
let start = ofst.add_state();
ofst.set_start(start);
let final_state = ofst.add_state();
ofst.set_final(final_state, A::Weight::one());
{
let mut pairs = pairs.borrow_mut();
while pairs.len() <= final_state.as_usize() {
pairs.push((None, A::Weight::zero()));
}
pairs[final_state.as_usize()] = (Some(rstart), A::Weight::one());
}
heap.insert(final_state.as_usize());
let limit = distance[start_index].times(weight_threshold);
let mut taken: Vec<usize> = Vec::new();
let count_index = |state: &Option<A::StateId>| match state {
None => 0,
Some(state) => state.as_usize() + 1,
};
while let Some(state) = heap.pop() {
let pair = pairs.borrow()[state].clone();
let d = match &pair.0 {
None => A::Weight::one(),
Some(at) => owned_distance
.get(at.as_usize())
.cloned()
.unwrap_or_else(A::Weight::zero),
};
if natural_less(&limit, &d.times(&pair.1))
|| state_threshold.is_some_and(|limit| ofst.num_states() >= limit)
{
continue;
}
let index = count_index(&pair.0);
while taken.len() <= index {
taken.push(0);
}
taken[index] += 1;
if pair.0.is_none() {
ofst.add_arc(
start,
A::new(
A::Label::epsilon(),
A::Label::epsilon(),
A::Weight::one(),
A::StateId::from_usize(state),
),
);
if taken[index] == nshortest {
break;
}
continue;
}
if taken[index] > nshortest {
continue;
}
let at = pair.0.expect("just checked");
for arc in rfst.arcs(at) {
let weight = pair.1.times(&arc.weight().reverse().into());
let next = ofst.add_state();
pairs.borrow_mut().push((Some(arc.nextstate()), weight));
ofst.add_arc(
next,
A::new(
arc.ilabel(),
arc.olabel(),
arc.weight().reverse().into(),
A::StateId::from_usize(state),
),
);
heap.insert(next.as_usize());
}
let final_weight: A::Weight = rfst.final_weight(at).reverse().into();
if final_weight != A::Weight::zero() {
let weight = pair.1.times(&final_weight);
let next = ofst.add_state();
pairs.borrow_mut().push((None, weight));
ofst.add_arc(
next,
A::new(
A::Label::epsilon(),
A::Label::epsilon(),
final_weight,
A::StateId::from_usize(state),
),
);
heap.insert(next.as_usize());
}
}
connect(ofst);
let props = shortest_path_properties(ofst.properties(K_FST_PROPERTIES, false), false);
ofst.set_properties(props, K_FST_PROPERTIES);
Ok(())
}
pub fn shortest_path<A, F1, F2>(
ifst: &F1,
ofst: &mut F2,
opts: &ShortestPathOptions<A::Weight>,
) -> Result<(), OpenFstError>
where
A: Arc,
A::Weight: PathWeight,
F1: Fst<A> + ExpandedFst<A>,
F2: MutableFst<A> + ExpandedFst<A>,
<A::Weight as Weight>::ReverseWeight: Weight<ReverseWeight = A::Weight>,
<<A::Reverse as Arc>::Weight as Weight>::ReverseWeight: Into<A::Weight>,
{
if opts.nshortest == 0 {
ofst.delete_all_states();
return Ok(());
}
if opts.nshortest == 1
&& opts.weight_threshold == A::Weight::zero()
&& opts.state_threshold.is_none()
{
let distance: Distance<A::Weight> = Rc::new(RefCell::new(Vec::new()));
let comp = state_weight_compare::<A::StateId, A::Weight, _>(
Rc::clone(&distance),
natural_less_unchecked::<A::Weight>,
);
let comp = (A::Weight::properties() & PATH != 0).then_some(comp);
let mut queue = AutoQueue::new(ifst, comp);
let mut parent: Vec<Parent<A::StateId>> = Vec::new();
let best_final =
single_shortest_path(ifst, &distance, &mut queue, &mut parent, opts.first_path)?;
backtrace(ifst, ofst, &parent, best_final);
return Ok(());
}
let (rfst, shifted) = reversed_with_distance(ifst, opts.delta)?;
n_shortest_path(
&rfst,
ofst,
&shifted,
opts.nshortest,
opts.delta,
&opts.weight_threshold,
opts.state_threshold,
)
}
pub fn shortest_path_unique<A, F1, F2>(
ifst: &F1,
ofst: &mut F2,
opts: &ShortestPathOptions<A::Weight>,
max_states: Option<usize>,
) -> Result<(), OpenFstError>
where
A: Arc,
A::Weight: PathWeight,
<A::Reverse as Arc>::Weight: Divide + std::hash::Hash + Eq,
F1: Fst<A> + ExpandedFst<A>,
F2: MutableFst<A> + ExpandedFst<A>,
<A::Weight as Weight>::ReverseWeight: Weight<ReverseWeight = A::Weight>,
<<A::Reverse as Arc>::Weight as Weight>::ReverseWeight: Into<A::Weight>,
{
if ifst.properties(K_ACCEPTOR, true) & K_ACCEPTOR == 0 {
return Err(OpenFstError::InvalidOperation(
"ShortestPath: distinct input strings takes an acceptor; project the input onto \
one side first"
.into(),
));
}
if opts.nshortest <= 1 {
return shortest_path(ifst, ofst, opts);
}
let (rfst, shifted) = reversed_with_distance(ifst, opts.delta)?;
let in_dist: Vec<<A::Reverse as Arc>::Weight> = shifted.iter().map(Weight::reverse).collect();
let mut out_dist: Vec<<A::Reverse as Arc>::Weight> = Vec::new();
let mut dfst: VectorFst<A::Reverse> = VectorFst::new();
determinize_fsa_with_distance(
&rfst,
&mut dfst,
&DefaultCommonDivisor,
opts.delta,
max_states,
&in_dist,
&mut out_dist,
)?;
let distance: Vec<A::Weight> = out_dist.iter().map(Weight::reverse).collect();
n_shortest_path(
&dfst,
ofst,
&distance,
opts.nshortest,
opts.delta,
&opts.weight_threshold,
opts.state_threshold,
)
}
fn reversed_with_distance<A, F>(
ifst: &F,
delta: f32,
) -> Result<(VectorFst<A::Reverse>, Vec<A::Weight>), OpenFstError>
where
A: Arc,
A::Weight: PathWeight,
F: Fst<A> + ExpandedFst<A>,
<A::Weight as Weight>::ReverseWeight: Weight<ReverseWeight = A::Weight>,
{
let distance: Distance<A::Weight> = Rc::new(RefCell::new(Vec::new()));
let comp = state_weight_compare::<A::StateId, A::Weight, _>(
Rc::clone(&distance),
natural_less_unchecked::<A::Weight>,
);
let comp = (A::Weight::properties() & PATH != 0).then_some(comp);
{
let mut queue = AutoQueue::new(ifst, comp);
let sd_opts = ShortestDistanceOptions {
delta,
..ShortestDistanceOptions::new(AnyArcFilter)
};
shortest_distance_with(ifst, &distance, &mut queue, &sd_opts)?;
}
let forward = distance.borrow().clone();
let mut rfst: VectorFst<A::Reverse> = VectorFst::new();
reverse(ifst, &mut rfst, true);
let mut shifted: Vec<A::Weight> = Vec::with_capacity(forward.len() + 1);
let mut superinitial = A::Weight::zero();
if let Some(rstart) = rfst.start() {
for arc in rfst.arcs(rstart) {
let state = arc.nextstate().as_usize().wrapping_sub(1);
if let Some(weight) = forward.get(state) {
let reversed: A::Weight = arc.weight().reverse();
superinitial = superinitial.plus(&reversed.times(weight));
}
}
}
shifted.push(superinitial);
shifted.extend(forward);
Ok((rfst, shifted))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::shortest_distance::shortest_distance;
use crate::algorithms::test_support::{Rng, paths, random_acyclic_fst, visible_paths};
use crate::arc::StdArc;
use crate::fsts::vector_fst::StdVectorFst;
use crate::properties::K_FST_PROPERTIES;
use crate::weights::float_weight::TropicalWeight;
use std::collections::BTreeMap;
fn best(fst: &StdVectorFst, n: usize) -> StdVectorFst {
let mut out = StdVectorFst::new();
shortest_path(
fst,
&mut out,
&ShortestPathOptions {
nshortest: n,
..Default::default()
},
)
.unwrap();
out
}
fn weights(fst: &StdVectorFst) -> Vec<f32> {
let mut out: Vec<f32> = paths(fst, 24)
.into_iter()
.map(|(_, _, weight)| weight.value())
.collect();
out.sort_by(f32::total_cmp);
out
}
fn fan() -> StdVectorFst {
let mut fst = StdVectorFst::new();
for _ in 0..3 {
fst.add_state();
}
fst.set_start(0);
fst.add_arc(0, StdArc::new(1, 1, TropicalWeight(1.0), 1));
fst.add_arc(0, StdArc::new(2, 2, TropicalWeight(3.0), 1));
fst.add_arc(0, StdArc::new(3, 3, TropicalWeight(6.0), 2));
fst.set_final(1, TropicalWeight::one());
fst.set_final(2, TropicalWeight::one());
fst.properties(K_FST_PROPERTIES, true);
fst
}
#[test]
fn the_shortest_path_is_the_lightest_one() {
let out = best(&fan(), 1);
assert_eq!(weights(&out), vec![1.0]);
let labels: Vec<i32> = paths(&out, 24)
.into_iter()
.flat_map(|(ilabels, _, _)| ilabels)
.collect();
assert_eq!(labels, vec![1]);
}
#[test]
fn the_shortest_path_goes_the_cheaper_way_round() {
let mut fst = StdVectorFst::new();
for _ in 0..4 {
fst.add_state();
}
fst.set_start(0);
fst.add_arc(0, StdArc::new(1, 1, TropicalWeight(1.0), 1));
fst.add_arc(0, StdArc::new(2, 2, TropicalWeight(4.0), 2));
fst.add_arc(1, StdArc::new(3, 3, TropicalWeight(5.0), 3));
fst.add_arc(2, StdArc::new(4, 4, TropicalWeight(1.0), 3));
fst.set_final(3, TropicalWeight::one());
fst.properties(K_FST_PROPERTIES, true);
let out = best(&fst, 1);
let labels: Vec<i32> = paths(&out, 24)
.into_iter()
.flat_map(|(ilabels, _, _)| ilabels)
.collect();
assert_eq!(labels, vec![2, 4], "4 + 1 beats 1 + 5");
assert_eq!(weights(&out), vec![5.0]);
}
#[test]
fn the_n_shortest_paths_are_the_n_lightest() {
let fst = fan();
assert_eq!(weights(&best(&fst, 2)), vec![1.0, 3.0]);
assert_eq!(weights(&best(&fst, 3)), vec![1.0, 3.0, 6.0]);
assert_eq!(weights(&best(&fst, 10)), vec![1.0, 3.0, 6.0]);
}
#[test]
fn asking_for_no_paths_gives_none() {
assert_eq!(best(&fan(), 0).num_states(), 0);
}
#[test]
fn an_fst_with_no_accepting_path_has_none() {
let mut fst = StdVectorFst::new();
for _ in 0..2 {
fst.add_state();
}
fst.set_start(0);
fst.add_arc(0, StdArc::new(1, 1, TropicalWeight::one(), 1));
fst.properties(K_FST_PROPERTIES, true);
assert!(weights(&best(&fst, 1)).is_empty());
assert!(weights(&best(&fst, 3)).is_empty());
assert!(weights(&unique_best(&fst, 3)).is_empty());
assert_eq!(best(&StdVectorFst::new(), 1).num_states(), 0);
assert_eq!(unique_best(&StdVectorFst::new(), 3).num_states(), 0);
}
#[test]
fn the_shortest_path_weighs_what_the_shortest_distance_says() {
let mut rng = Rng::new(0x0000_5B47_u64);
let mut checked = 0;
for round in 0..200 {
let fst = random_acyclic_fst(&mut rng, 6);
let total = shortest_distance(&fst, SHORTEST_DELTA).unwrap();
if total == TropicalWeight::zero() {
continue;
}
checked += 1;
let got = weights(&best(&fst, 1));
assert_eq!(got.len(), 1, "round {round}");
assert!(
(got[0] - total.value()).abs() < 1e-4,
"round {round}: {} against {}",
got[0],
total.value()
);
}
assert!(checked > 50, "only {checked} FSTs accepted anything");
}
#[test]
fn the_n_shortest_paths_are_the_n_lightest_of_all_of_them() {
let mut rng = Rng::new(0x0000_9E57_u64);
let mut checked = 0;
for round in 0..200 {
let fst = random_acyclic_fst(&mut rng, 6);
let mut all: Vec<f32> = paths(&fst, 16)
.into_iter()
.map(|(_, _, weight)| weight.value())
.collect();
if all.is_empty() {
continue;
}
all.sort_by(f32::total_cmp);
checked += 1;
for n in [1usize, 2, 3, 5] {
let want: Vec<f32> = all.iter().take(n).copied().collect();
let got = weights(&best(&fst, n));
assert_eq!(got.len(), want.len(), "round {round}, n = {n}");
for (got, want) in got.iter().zip(&want) {
assert!(
(got - want).abs() < 1e-4,
"round {round}, n = {n}: {got} against {want}"
);
}
}
}
assert!(checked > 50, "only {checked} FSTs accepted anything");
}
#[test]
fn a_weight_threshold_drops_what_is_too_heavy() {
let with = |threshold: f32| {
let mut out = StdVectorFst::new();
shortest_path(
&fan(),
&mut out,
&ShortestPathOptions {
nshortest: 5,
weight_threshold: TropicalWeight(threshold),
..Default::default()
},
)
.unwrap();
weights(&out)
};
assert_eq!(with(2.5), vec![1.0, 3.0], "the limit is 3.5");
assert_eq!(with(1.0), vec![1.0], "the limit is 2.0");
assert_eq!(with(10.0), vec![1.0, 3.0, 6.0], "the limit is 11.0");
}
#[test]
fn a_single_path_honours_the_pruning_thresholds() {
let run = |weight_threshold, state_threshold| {
let mut out = StdVectorFst::new();
shortest_path(
&fan(),
&mut out,
&ShortestPathOptions {
nshortest: 1,
weight_threshold,
state_threshold,
..Default::default()
},
)
.unwrap();
out
};
assert!(
weights(&run(TropicalWeight(-0.5), None)).is_empty(),
"a threshold lighter than one excludes even the best path"
);
assert_eq!(
run(TropicalWeight::zero(), Some(1)).num_states(),
0,
"the state cap applies when one path was requested"
);
assert_eq!(
weights(&run(TropicalWeight(1.0), None)),
vec![1.0],
"a usable threshold still returns the best path"
);
}
fn unique_best(fst: &StdVectorFst, n: usize) -> StdVectorFst {
let mut out = StdVectorFst::new();
shortest_path_unique(
fst,
&mut out,
&ShortestPathOptions {
nshortest: n,
..Default::default()
},
None,
)
.unwrap();
out
}
fn strings(fst: &StdVectorFst) -> Vec<Vec<i32>> {
let mut out: Vec<Vec<i32>> = visible_paths(fst, 24)
.into_iter()
.map(|(ilabels, _, _)| ilabels)
.collect();
out.sort();
out
}
fn twins() -> StdVectorFst {
let mut fst = StdVectorFst::new();
for _ in 0..4 {
fst.add_state();
}
fst.set_start(0);
fst.add_arc(0, StdArc::new(1, 1, TropicalWeight(1.0), 1));
fst.add_arc(0, StdArc::new(1, 1, TropicalWeight(2.0), 1));
fst.add_arc(0, StdArc::new(3, 3, TropicalWeight(5.0), 2));
fst.add_arc(1, StdArc::new(2, 2, TropicalWeight::one(), 3));
fst.add_arc(2, StdArc::new(2, 2, TropicalWeight::one(), 3));
fst.set_final(3, TropicalWeight::one());
fst.properties(K_FST_PROPERTIES, true);
fst
}
#[test]
fn the_same_string_twice_comes_back_once() {
assert_eq!(
weights(&best(&twins(), 3)),
vec![1.0, 2.0, 5.0],
"both ways of spelling 1 2 are paths"
);
assert_eq!(
weights(&unique_best(&twins(), 3)),
vec![1.0, 5.0],
"the heavier way of spelling 1 2 is not an answer of its own"
);
assert_eq!(
strings(&unique_best(&twins(), 3)),
vec![vec![1, 2], vec![3, 2]]
);
}
#[test]
fn one_distinct_path_is_the_shortest_path() {
assert_eq!(
weights(&unique_best(&twins(), 1)),
weights(&best(&twins(), 1))
);
assert_eq!(strings(&unique_best(&twins(), 1)), vec![vec![1, 2]]);
assert_eq!(unique_best(&twins(), 0).num_states(), 0);
}
#[test]
fn the_n_distinct_paths_are_the_n_lightest_strings() {
let mut rng = Rng::new(0x0000_D157_u64);
let mut checked = 0;
for round in 0..200 {
let fst = random_acyclic_fst(&mut rng, 6);
let mut per_string: BTreeMap<Vec<i32>, f32> = BTreeMap::new();
for (ilabels, _, weight) in paths(&fst, 16) {
per_string
.entry(ilabels)
.and_modify(|best| *best = best.min(weight.value()))
.or_insert(weight.value());
}
if per_string.is_empty() {
continue;
}
checked += 1;
let mut all: Vec<f32> = per_string.values().copied().collect();
all.sort_by(f32::total_cmp);
for n in [2usize, 3, 5] {
let out = unique_best(&fst, n);
let got = weights(&out);
let want: Vec<f32> = all.iter().take(n).copied().collect();
assert_eq!(got.len(), want.len(), "round {round}, n = {n}");
for (got, want) in got.iter().zip(&want) {
assert!(
(got - want).abs() < 1e-4,
"round {round}, n = {n}: {got} against {want}"
);
}
let spelled = strings(&out);
let mut distinct = spelled.clone();
distinct.dedup();
assert_eq!(
distinct.len(),
spelled.len(),
"round {round}, n = {n}: a string came back twice"
);
}
}
assert!(checked > 50, "only {checked} FSTs accepted anything");
}
#[test]
fn distinct_input_strings_takes_an_acceptor() {
let mut fst = StdVectorFst::new();
for _ in 0..2 {
fst.add_state();
}
fst.set_start(0);
fst.add_arc(0, StdArc::new(1, 2, TropicalWeight::one(), 1));
fst.set_final(1, TropicalWeight::one());
fst.properties(K_FST_PROPERTIES, true);
let mut out = StdVectorFst::new();
let err = shortest_path_unique(
&fst,
&mut out,
&ShortestPathOptions {
nshortest: 2,
..Default::default()
},
None,
)
.unwrap_err();
assert!(format!("{err}").contains("acceptor"), "{err}");
}
}