use std::sync::Arc;
use lling_llang::prelude::{
LazyState, Semiring, StateId, StateSource, TropicalWeight, WeightedTransition,
};
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
#[cfg(feature = "phonetic-rules")]
use liblevenshtein::phonetic::nfa::{NFAChar, ProductAutomatonChar, ProductStateChar};
use libdictenstein::{Dictionary, DictionaryNode};
use crate::state_encoding;
#[cfg(feature = "phonetic-rules")]
#[derive(Clone)]
pub struct PhoneticStateSource<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,
product: Arc<ProductAutomatonChar>,
max_distance: u8,
phonetic_weight: f64,
max_product_states: u32,
node_registry: Arc<std::sync::RwLock<NodeRegistry<D::Node>>>,
product_state_registry: Arc<std::sync::RwLock<ProductStateRegistry>>,
}
struct NodeRegistry<N: DictionaryNode> {
node_to_id: FxHashMap<u64, u32>,
id_to_node: Vec<N>,
}
impl<N: DictionaryNode> NodeRegistry<N> {
fn new(root: N) -> Self {
let mut registry = Self {
node_to_id: FxHashMap::default(),
id_to_node: Vec::new(),
};
registry.register_node(root, 0);
registry
}
fn register_node(&mut self, node: N, path_hash: u64) -> u32 {
if let Some(&id) = self.node_to_id.get(&path_hash) {
return id;
}
let id = self.id_to_node.len() as u32;
self.node_to_id.insert(path_hash, id);
self.id_to_node.push(node);
id
}
fn get_node(&self, id: u32) -> Option<&N> {
self.id_to_node.get(id as usize)
}
}
#[cfg(feature = "phonetic-rules")]
struct ProductStateRegistry {
state_to_id: FxHashMap<ProductStateKey, u32>,
id_to_state: Vec<ProductStateChar>,
}
#[cfg(feature = "phonetic-rules")]
#[derive(Clone, PartialEq, Eq, Hash)]
struct ProductStateKey(Vec<u8>);
#[cfg(feature = "phonetic-rules")]
impl ProductStateRegistry {
fn new(initial_state: ProductStateChar) -> Self {
let mut registry = Self {
state_to_id: FxHashMap::default(),
id_to_state: Vec::new(),
};
registry.register_state(initial_state);
registry
}
fn register_state(&mut self, state: ProductStateChar) -> u32 {
let key = Self::state_to_key(&state);
if let Some(&id) = self.state_to_id.get(&key) {
return id;
}
let id = self.id_to_state.len() as u32;
self.state_to_id.insert(key, id);
self.id_to_state.push(state);
id
}
fn get_state(&self, id: u32) -> Option<&ProductStateChar> {
self.id_to_state.get(id as usize)
}
fn state_to_key(state: &ProductStateChar) -> ProductStateKey {
let mut bytes = Vec::with_capacity(state.nfa_states.len() * 4 + 1);
for &s in &state.nfa_states {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes.push(state.edit_distance());
ProductStateKey(bytes)
}
fn len(&self) -> usize {
self.id_to_state.len()
}
}
#[cfg(feature = "phonetic-rules")]
impl<D> PhoneticStateSource<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, nfa: NFAChar, max_distance: u8) -> Self {
Self::with_phonetic_weight(dictionary, nfa, max_distance, 0.0)
}
pub fn with_phonetic_weight(
dictionary: &D,
nfa: NFAChar,
max_distance: u8,
phonetic_weight: f64,
) -> Self {
let product =
ProductAutomatonChar::with_phonetic_weight(nfa, max_distance, phonetic_weight);
let initial_state = product.initial_state();
let max_product_states = ((max_distance as u32 + 1) * 1000).max(10_000);
let root = dictionary.root();
let node_registry = NodeRegistry::new(root);
let product_state_registry = ProductStateRegistry::new(initial_state);
Self {
dictionary: dictionary.clone(),
product: Arc::new(product),
max_distance,
phonetic_weight,
max_product_states,
node_registry: Arc::new(std::sync::RwLock::new(node_registry)),
product_state_registry: Arc::new(std::sync::RwLock::new(product_state_registry)),
}
}
pub fn max_distance(&self) -> u8 {
self.max_distance
}
pub fn phonetic_weight(&self) -> f64 {
self.phonetic_weight
}
fn compute_transitions(
&self,
dict_node_id: u32,
product_state_id: u32,
) -> (
bool,
TropicalWeight,
SmallVec<[WeightedTransition<char, TropicalWeight>; 4]>,
) {
let node_registry = self.node_registry.read().expect("Lock poisoned");
let product_registry = self.product_state_registry.read().expect("Lock poisoned");
let dict_node = match node_registry.get_node(dict_node_id) {
Some(node) => node.clone(),
None => {
return (false, TropicalWeight::zero(), SmallVec::new());
}
};
let product_state = match product_registry.get_state(product_state_id) {
Some(state) => state.clone(),
None => {
return (false, TropicalWeight::zero(), SmallVec::new());
}
};
drop(node_registry);
drop(product_registry);
let mut transitions = SmallVec::new();
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 successors = self.product.transition(&product_state, dict_char);
for successor in successors {
let successor_id = {
let mut registry = self.product_state_registry.write().expect("Lock poisoned");
registry.register_state(successor.clone())
};
let cost = if successor.edit_distance() > product_state.edit_distance() {
1.0
} else {
self.phonetic_weight
};
let from_state =
state_encoding::encode(dict_node_id, product_state_id, self.max_product_states);
let target_state =
state_encoding::encode(child_node_id, successor_id, self.max_product_states);
transitions.push(WeightedTransition::new(
from_state,
Some(dict_char),
Some(dict_char),
target_state,
TropicalWeight::new(cost),
));
}
}
let is_final = dict_node.is_final() && self.product.is_accepting(&product_state);
let final_weight = if is_final {
TropicalWeight::new(product_state.edit_distance() as f64)
} else {
TropicalWeight::zero()
};
(is_final, final_weight, transitions)
}
}
#[cfg(feature = "phonetic-rules")]
impl<D> StateSource<char, TropicalWeight> for PhoneticStateSource<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, product_state_id) =
state_encoding::decode(state, self.max_product_states);
let (is_final, final_weight, transitions) =
self.compute_transitions(dict_node_id, product_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_product_states)
}
fn num_states_hint(&self) -> Option<usize> {
let dict_size = self.dictionary.len().unwrap_or(1000);
let product_registry = self.product_state_registry.read().expect("Lock poisoned");
let product_states = product_registry.len();
Some((dict_size * product_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)]
#[cfg(feature = "phonetic-rules")]
mod tests {
use super::*;
use liblevenshtein::phonetic::nfa::compiler::compile;
use liblevenshtein::phonetic::regex::parse;
use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
#[test]
fn test_phonetic_state_source_creation() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["phone", "fone", "help"]);
let nfa = compile(&parse("(ph|f)one").expect("parse")).expect("compile");
let source = PhoneticStateSource::new(&dict, nfa, 2);
assert_eq!(source.max_distance(), 2);
assert_eq!(source.phonetic_weight(), 0.0);
}
#[test]
fn test_phonetic_state_source_start_state() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["test"]);
let nfa = compile(&parse("test").expect("parse")).expect("compile");
let source = PhoneticStateSource::new(&dict, nfa, 1);
let start = source.start();
let (dict_node, product_state) = state_encoding::decode(start, source.max_product_states);
assert_eq!(dict_node, 0);
assert_eq!(product_state, 0);
}
#[test]
fn test_phonetic_state_source_with_weight() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["phone"]);
let nfa = compile(&parse("(ph|f)one").expect("parse")).expect("compile");
let source = PhoneticStateSource::with_phonetic_weight(&dict, nfa, 2, 0.5);
assert_eq!(source.phonetic_weight(), 0.5);
}
#[test]
fn test_phonetic_state_source_compute_state() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["phone", "help"]);
let nfa = compile(&parse("phone").expect("parse")).expect("compile");
let source = PhoneticStateSource::new(&dict, nfa, 1);
let start = source.start();
let state = source.compute_state(start);
assert!(state.is_computed());
}
#[test]
fn test_phonetic_state_source_num_states_hint() {
let dict = DynamicDawgChar::<()>::from_terms(vec!["test", "rest", "best"]);
let nfa = compile(&parse("test").expect("parse")).expect("compile");
let source = PhoneticStateSource::new(&dict, nfa, 1);
let hint = source.num_states_hint();
assert!(hint.is_some());
assert!(hint.expect("expected Some hint in test") > 0);
}
}