use lling_llang::prelude::{
LazyState, LazyWfst, Semiring, StateId, StateSource, TropicalWeight, WeightedTransition, Wfst,
};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use liblevenshtein::transducer::Algorithm;
use libdictenstein::{Dictionary, DictionaryNode};
use crate::state_encoding;
use crate::state_source::LevenshteinStateSource;
#[derive(Clone)]
pub struct LevenshteinWfst<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
state_source: LevenshteinStateSource<D>,
cache: FxHashMap<StateId, CachedState>,
max_distance: usize,
algorithm: Algorithm,
max_automaton_states: u32,
cache_policy: lling_llang::wfst::CachePolicy,
max_cache_size: usize,
}
#[derive(Clone)]
struct CachedState {
is_final: bool,
final_weight: TropicalWeight,
transitions: SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
}
const DEFAULT_MAX_CACHE_SIZE: usize = 100_000;
impl<D> LevenshteinWfst<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
pub fn new(dictionary: &D, query: &str, max_distance: usize) -> Self {
Self::with_algorithm(dictionary, query, max_distance, Algorithm::Standard)
}
pub fn with_algorithm(
dictionary: &D,
query: &str,
max_distance: usize,
algorithm: Algorithm,
) -> Self {
let state_source =
LevenshteinStateSource::with_algorithm(dictionary, query, max_distance, algorithm);
let query_len = query.chars().count();
let max_automaton_states =
state_encoding::estimate_automaton_states(query_len, max_distance);
Self {
state_source,
cache: FxHashMap::default(),
max_distance,
algorithm,
max_automaton_states,
cache_policy: lling_llang::wfst::CachePolicy::CacheAll,
max_cache_size: DEFAULT_MAX_CACHE_SIZE,
}
}
pub fn max_distance(&self) -> usize {
self.max_distance
}
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
pub fn query(&self) -> String {
self.state_source.query()
}
pub fn set_max_cache_size(&mut self, size: usize) {
self.max_cache_size = size;
}
fn ensure_state(&mut self, state: StateId) {
if self.cache.contains_key(&state) {
return;
}
let lazy_state = self.state_source.compute_state(state);
let cached = match lazy_state {
LazyState::Computed {
is_final,
final_weight,
transitions,
} => CachedState {
is_final,
final_weight,
transitions,
},
LazyState::Pending => {
CachedState {
is_final: false,
final_weight: TropicalWeight::zero(),
transitions: SmallVec::new(),
}
}
};
if let lling_llang::wfst::CachePolicy::Lru { max_states } = self.cache_policy {
let limit = if max_states > 0 {
max_states
} else {
self.max_cache_size
};
if self.cache.len() >= limit {
let to_remove = (self.cache.len() / 10).max(1);
let keys: Vec<_> = self.cache.keys().take(to_remove).copied().collect();
for key in keys {
self.cache.remove(&key);
}
}
}
self.cache.insert(state, cached);
}
}
impl<D> Wfst<char, TropicalWeight> for LevenshteinWfst<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
fn start(&self) -> StateId {
self.state_source.start()
}
fn is_final(&self, state: StateId) -> bool {
self.cache.get(&state).map(|s| s.is_final).unwrap_or(false)
}
fn final_weight(&self, state: StateId) -> TropicalWeight {
self.cache
.get(&state)
.map(|s| s.final_weight)
.unwrap_or_else(TropicalWeight::zero)
}
fn transitions(&self, state: StateId) -> &[WeightedTransition<char, TropicalWeight>] {
static EMPTY: &[WeightedTransition<char, TropicalWeight>] = &[];
self.cache
.get(&state)
.map(|s| s.transitions.as_slice())
.unwrap_or(EMPTY)
}
fn num_states(&self) -> usize {
self.cache.len()
}
#[inline]
fn is_empty(&self) -> bool {
false
}
#[inline]
fn is_valid_state(&self, state: StateId) -> bool {
let (dict_node, automaton_state) = state_encoding::decode(state, self.max_automaton_states);
automaton_state < self.max_automaton_states || dict_node == 0
}
}
impl<D> LazyWfst<char, TropicalWeight> for LevenshteinWfst<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
fn is_expanded(&self, state: StateId) -> bool {
self.cache.contains_key(&state)
}
fn expand(&mut self, state: StateId) {
self.ensure_state(state);
}
fn transitions_lazy(&mut self, state: StateId) -> &[WeightedTransition<char, TropicalWeight>] {
self.ensure_state(state);
self.transitions(state)
}
fn cache_policy(&self) -> lling_llang::wfst::CachePolicy {
self.cache_policy
}
fn set_cache_policy(&mut self, policy: lling_llang::wfst::CachePolicy) {
self.cache_policy = policy;
}
fn computed_states(&self) -> usize {
self.cache.len()
}
fn clear_cache(&mut self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
#[test]
fn test_levenshtein_wfst_creation() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help", "world"]);
let wfst = LevenshteinWfst::new(&dict, "helo", 2);
assert_eq!(wfst.max_distance(), 2);
assert_eq!(wfst.algorithm(), Algorithm::Standard);
}
#[test]
fn test_levenshtein_wfst_start_state() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help", "world"]);
let wfst = LevenshteinWfst::new(&dict, "helo", 2);
let start = wfst.start();
let (dict_node, auto_state) = state_encoding::decode(start, wfst.max_automaton_states);
assert_eq!(dict_node, 0);
assert_eq!(auto_state, 0);
}
#[test]
fn test_levenshtein_wfst_with_algorithm() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["test"]);
let wfst = LevenshteinWfst::with_algorithm(&dict, "tset", 2, Algorithm::Transposition);
assert_eq!(wfst.algorithm(), Algorithm::Transposition);
}
#[test]
fn test_levenshtein_wfst_expand_state() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help"]);
let mut wfst = LevenshteinWfst::new(&dict, "helo", 2);
let start = wfst.start();
assert!(!wfst.is_expanded(start));
wfst.expand(start);
assert!(wfst.is_expanded(start));
let transitions = wfst.transitions(start);
assert!(!transitions.is_empty());
}
#[test]
fn test_levenshtein_wfst_utf8_support() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["café", "naïve", "北京"]);
let mut wfst = LevenshteinWfst::new(&dict, "cafe", 1);
let start = wfst.start();
wfst.expand(start);
assert!(wfst.computed_states() > 0);
}
#[test]
fn test_levenshtein_wfst_cache_policy() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["test"]);
let mut wfst = LevenshteinWfst::new(&dict, "test", 1);
assert!(matches!(
wfst.cache_policy(),
lling_llang::wfst::CachePolicy::CacheAll
));
wfst.set_cache_policy(lling_llang::wfst::CachePolicy::Lru { max_states: 1000 });
assert!(matches!(
wfst.cache_policy(),
lling_llang::wfst::CachePolicy::Lru { .. }
));
}
}