use std::sync::Arc;
use lling_llang::prelude::{
LazyState, Semiring, StateId, StateSource, TropicalWeight, WeightedTransition,
};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use liblevenshtein::transducer::Algorithm;
use libdictenstein::{Dictionary, DictionaryNode};
use crate::state_encoding;
#[derive(Clone)]
pub struct LevenshteinStateSource<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
dictionary: D,
query_chars: Arc<Vec<char>>,
max_distance: usize,
algorithm: Algorithm,
max_automaton_states: u32,
node_registry: Arc<std::sync::RwLock<NodeRegistry<D::Node>>>,
}
struct NodeRegistry<N: DictionaryNode> {
node_to_id: FxHashMap<NodeKey, u32>,
id_to_node: Vec<N>,
next_id: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct NodeKey(u64);
impl<N: DictionaryNode> NodeRegistry<N> {
fn new(root: N) -> Self {
let mut registry = Self {
node_to_id: FxHashMap::default(),
id_to_node: Vec::new(),
next_id: 0,
};
registry.register_node(root, 0);
registry
}
fn register_node(&mut self, node: N, path_hash: u64) -> u32 {
let key = NodeKey(path_hash);
if let Some(&id) = self.node_to_id.get(&key) {
return id;
}
let id = self.next_id;
self.next_id += 1;
self.node_to_id.insert(key, id);
self.id_to_node.push(node);
id
}
fn get_node(&self, id: u32) -> Option<&N> {
self.id_to_node.get(id as usize)
}
}
impl<D> LevenshteinStateSource<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 query_chars: Vec<char> = query.chars().collect();
let max_automaton_states =
state_encoding::estimate_automaton_states(query_chars.len(), max_distance);
let root = dictionary.root();
let registry = NodeRegistry::new(root);
Self {
dictionary: dictionary.clone(),
query_chars: Arc::new(query_chars),
max_distance,
algorithm,
max_automaton_states,
node_registry: Arc::new(std::sync::RwLock::new(registry)),
}
}
pub fn query(&self) -> String {
self.query_chars.iter().collect()
}
fn compute_transitions(
&self,
dict_node_id: u32,
query_pos: u32,
) -> (
bool,
TropicalWeight,
SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
) {
let registry = self.node_registry.read().expect("Lock poisoned");
let dict_node = match registry.get_node(dict_node_id) {
Some(node) => node.clone(),
None => {
return (false, TropicalWeight::zero(), SmallVec::new());
}
};
drop(registry);
let mut transitions = SmallVec::new();
let query_len = self.query_chars.len() as u32;
let pos = query_pos as usize;
for (unit, child_node) in dict_node.edges() {
let dict_char: char = unit.into();
let child_node_id = {
let mut registry = self.node_registry.write().expect("Lock poisoned");
let path_hash = compute_path_hash(dict_node_id, dict_char);
registry.register_node(child_node.clone(), path_hash)
};
let from_state =
state_encoding::encode(dict_node_id, query_pos, self.max_automaton_states);
if pos < self.query_chars.len() {
let query_char = self.query_chars[pos];
let cost = if query_char == dict_char { 0 } else { 1 };
let target_state =
state_encoding::encode(child_node_id, query_pos + 1, self.max_automaton_states);
transitions.push(WeightedTransition::new(
from_state,
Some(dict_char),
Some(dict_char),
target_state,
TropicalWeight::new(cost as f64),
));
}
let insert_target = state_encoding::encode(
child_node_id,
query_pos, self.max_automaton_states,
);
transitions.push(WeightedTransition::new(
from_state,
Some(dict_char),
Some(dict_char),
insert_target,
TropicalWeight::new(1.0),
));
if self.algorithm == Algorithm::Transposition && pos + 1 < self.query_chars.len() {
let query_char = self.query_chars[pos];
let next_query_char = self.query_chars[pos + 1];
if dict_char == next_query_char && query_char != dict_char {
}
}
}
if pos < self.query_chars.len() {
let delete_target = state_encoding::encode(
dict_node_id, query_pos + 1, self.max_automaton_states,
);
let from_state =
state_encoding::encode(dict_node_id, query_pos, self.max_automaton_states);
transitions.push(WeightedTransition::new(
from_state,
None, None, delete_target,
TropicalWeight::new(1.0),
));
}
let is_final = dict_node.is_final();
let remaining = query_len.saturating_sub(query_pos) as usize;
let can_accept = remaining <= self.max_distance;
let final_weight = if is_final && can_accept {
TropicalWeight::new(remaining as f64)
} else {
TropicalWeight::zero() };
(is_final && can_accept, final_weight, transitions)
}
}
impl<D> StateSource<char, TropicalWeight> for LevenshteinStateSource<D>
where
D: Dictionary + Clone + Send + Sync,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
fn compute_state(&self, state: StateId) -> LazyState<char, TropicalWeight> {
let (dict_node_id, automaton_state_id) =
state_encoding::decode(state, self.max_automaton_states);
let (is_final, final_weight, transitions) =
self.compute_transitions(dict_node_id, automaton_state_id);
if is_final {
LazyState::final_state(final_weight, transitions)
} else {
LazyState::non_final(transitions)
}
}
fn start(&self) -> StateId {
state_encoding::encode(0, 0, self.max_automaton_states)
}
fn num_states_hint(&self) -> Option<usize> {
let dict_size = self.dictionary.len().unwrap_or(1000);
let automaton_states = self.max_automaton_states as usize;
Some((dict_size * automaton_states).min(1_000_000))
}
}
#[inline]
fn compute_path_hash(parent_id: u32, edge_label: char) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = rustc_hash::FxHasher::default();
parent_id.hash(&mut hasher);
edge_label.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
#[test]
fn test_state_source_creation() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help", "world"]);
let source = LevenshteinStateSource::new(&dict, "helo", 2);
assert_eq!(source.start(), 0);
assert!(source.num_states_hint().is_some());
}
#[test]
fn test_state_source_compute_start_state() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["hello", "help"]);
let source = LevenshteinStateSource::new(&dict, "helo", 2);
let start = source.start();
let state = source.compute_state(start);
assert!(state.is_computed());
}
#[test]
fn test_state_source_clone() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["test"]);
let source = LevenshteinStateSource::new(&dict, "tset", 2);
let cloned = source.clone();
assert_eq!(source.start(), cloned.start());
}
}